-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AccountController < ApplicationController
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
-
# prevents login action to be filtered by check_if_login_required application scope filter
-
1
skip_before_filter :check_if_login_required
-
-
# Login request and validation
-
1
def login
-
246
if request.get?
-
123
logout_user
-
else
-
123
authenticate_user
-
end
-
rescue AuthSourceException => e
-
logger.error "An error occured when authenticating #{params[:username]}: #{e.message}"
-
render_error :message => e.message
-
end
-
-
# Log out current user and redirect to welcome page
-
1
def logout
-
4
logout_user
-
4
redirect_to home_url
-
end
-
-
# Enable user to choose a new password
-
1
def lost_password
-
redirect_to(home_url) && return unless Setting.lost_password?
-
if params[:token]
-
@token = Token.find_by_action_and_value("recovery", params[:token])
-
redirect_to(home_url) && return unless @token and !@token.expired?
-
@user = @token.user
-
if request.post?
-
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
-
if @user.save
-
@token.destroy
-
flash[:notice] = l(:notice_account_password_updated)
-
redirect_to :action => 'login'
-
return
-
end
-
end
-
render :template => "account/password_recovery"
-
return
-
else
-
if request.post?
-
user = User.find_by_mail(params[:mail])
-
# user not found in db
-
(flash.now[:error] = l(:notice_account_unknown_email); return) unless user
-
# user uses an external authentification
-
(flash.now[:error] = l(:notice_can_t_change_password); return) if user.auth_source_id
-
# create a new token for password recovery
-
token = Token.new(:user => user, :action => "recovery")
-
if token.save
-
Mailer.lost_password(token).deliver
-
flash[:notice] = l(:notice_account_lost_email_sent)
-
redirect_to :action => 'login'
-
return
-
end
-
end
-
end
-
end
-
-
# User self-registration
-
1
def register
-
redirect_to(home_url) && return unless Setting.self_registration? || session[:auth_source_registration]
-
if request.get?
-
session[:auth_source_registration] = nil
-
@user = User.new(:language => Setting.default_language)
-
else
-
@user = User.new
-
@user.safe_attributes = params[:user]
-
@user.admin = false
-
@user.register
-
if session[:auth_source_registration]
-
@user.activate
-
@user.login = session[:auth_source_registration][:login]
-
@user.auth_source_id = session[:auth_source_registration][:auth_source_id]
-
if @user.save
-
session[:auth_source_registration] = nil
-
self.logged_user = @user
-
flash[:notice] = l(:notice_account_activated)
-
redirect_to :controller => 'my', :action => 'account'
-
end
-
else
-
@user.login = params[:user][:login]
-
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
-
-
case Setting.self_registration
-
when '1'
-
register_by_email_activation(@user)
-
when '3'
-
register_automatically(@user)
-
else
-
register_manually_by_administrator(@user)
-
end
-
end
-
end
-
end
-
-
# Token based account activation
-
1
def activate
-
redirect_to(home_url) && return unless Setting.self_registration? && params[:token]
-
token = Token.find_by_action_and_value('register', params[:token])
-
redirect_to(home_url) && return unless token and !token.expired?
-
user = token.user
-
redirect_to(home_url) && return unless user.registered?
-
user.activate
-
if user.save
-
token.destroy
-
flash[:notice] = l(:notice_account_activated)
-
end
-
redirect_to :action => 'login'
-
end
-
-
1
private
-
-
1
def authenticate_user
-
123
if Setting.openid? && using_open_id?
-
open_id_authenticate(params[:openid_url])
-
else
-
123
password_authentication
-
end
-
end
-
-
1
def password_authentication
-
123
user = User.try_to_login(params[:username], params[:password])
-
-
123
if user.nil?
-
invalid_credentials
-
123
elsif user.new_record?
-
onthefly_creation_failed(user, {:login => user.login, :auth_source_id => user.auth_source_id })
-
else
-
# Valid user
-
123
successful_authentication(user)
-
end
-
end
-
-
1
def open_id_authenticate(openid_url)
-
authenticate_with_open_id(openid_url, :required => [:nickname, :fullname, :email], :return_to => signin_url, :method => :post) do |result, identity_url, registration|
-
if result.successful?
-
user = User.find_or_initialize_by_identity_url(identity_url)
-
if user.new_record?
-
# Self-registration off
-
redirect_to(home_url) && return unless Setting.self_registration?
-
-
# Create on the fly
-
user.login = registration['nickname'] unless registration['nickname'].nil?
-
user.mail = registration['email'] unless registration['email'].nil?
-
user.firstname, user.lastname = registration['fullname'].split(' ') unless registration['fullname'].nil?
-
user.random_password
-
user.register
-
-
case Setting.self_registration
-
when '1'
-
register_by_email_activation(user) do
-
onthefly_creation_failed(user)
-
end
-
when '3'
-
register_automatically(user) do
-
onthefly_creation_failed(user)
-
end
-
else
-
register_manually_by_administrator(user) do
-
onthefly_creation_failed(user)
-
end
-
end
-
else
-
# Existing record
-
if user.active?
-
successful_authentication(user)
-
else
-
account_pending
-
end
-
end
-
end
-
end
-
end
-
-
1
def successful_authentication(user)
-
# Valid user
-
123
self.logged_user = user
-
# generate a key and set cookie if autologin
-
123
if params[:autologin] && Setting.autologin?
-
set_autologin_cookie(user)
-
end
-
123
call_hook(:controller_account_success_authentication_after, {:user => user })
-
123
redirect_back_or_default :controller => 'my', :action => 'page'
-
end
-
-
1
def set_autologin_cookie(user)
-
token = Token.create(:user => user, :action => 'autologin')
-
cookie_name = Redmine::Configuration['autologin_cookie_name'] || 'autologin'
-
cookie_options = {
-
:value => token.value,
-
:expires => 1.year.from_now,
-
:path => (Redmine::Configuration['autologin_cookie_path'] || '/'),
-
:secure => (Redmine::Configuration['autologin_cookie_secure'] ? true : false),
-
:httponly => true
-
}
-
cookies[cookie_name] = cookie_options
-
end
-
-
# Onthefly creation failed, display the registration form to fill/fix attributes
-
1
def onthefly_creation_failed(user, auth_source_options = { })
-
@user = user
-
session[:auth_source_registration] = auth_source_options unless auth_source_options.empty?
-
render :action => 'register'
-
end
-
-
1
def invalid_credentials
-
logger.warn "Failed login for '#{params[:username]}' from #{request.remote_ip} at #{Time.now.utc}"
-
flash.now[:error] = l(:notice_account_invalid_creditentials)
-
end
-
-
# Register a user for email activation.
-
#
-
# Pass a block for behavior when a user fails to save
-
1
def register_by_email_activation(user, &block)
-
token = Token.new(:user => user, :action => "register")
-
if user.save and token.save
-
Mailer.register(token).deliver
-
flash[:notice] = l(:notice_account_register_done)
-
redirect_to :action => 'login'
-
else
-
yield if block_given?
-
end
-
end
-
-
# Automatically register a user
-
#
-
# Pass a block for behavior when a user fails to save
-
1
def register_automatically(user, &block)
-
# Automatic activation
-
user.activate
-
user.last_login_on = Time.now
-
if user.save
-
self.logged_user = user
-
flash[:notice] = l(:notice_account_activated)
-
redirect_to :controller => 'my', :action => 'account'
-
else
-
yield if block_given?
-
end
-
end
-
-
# Manual activation by the administrator
-
#
-
# Pass a block for behavior when a user fails to save
-
1
def register_manually_by_administrator(user, &block)
-
if user.save
-
# Sends an email to the administrators
-
Mailer.account_activation_request(user).deliver
-
account_pending
-
else
-
yield if block_given?
-
end
-
end
-
-
1
def account_pending
-
flash[:notice] = l(:notice_account_pending)
-
redirect_to :action => 'login'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ActivitiesController < ApplicationController
-
1
menu_item :activity
-
1
before_filter :find_optional_project
-
1
accept_rss_auth :index
-
-
1
def index
-
@days = Setting.activity_days_default.to_i
-
-
if params[:from]
-
begin; @date_to = params[:from].to_date + 1; rescue; end
-
end
-
-
@date_to ||= Date.today + 1
-
@date_from = @date_to - @days
-
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
-
@author = (params[:user_id].blank? ? nil : User.active.find(params[:user_id]))
-
-
@activity = Redmine::Activity::Fetcher.new(User.current, :project => @project,
-
:with_subprojects => @with_subprojects,
-
:author => @author)
-
@activity.scope_select {|t| !params["show_#{t}"].nil?}
-
@activity.scope = (@author.nil? ? :default : :all) if @activity.scope.empty?
-
-
events = @activity.events(@date_from, @date_to)
-
-
if events.empty? || stale?(:etag => [@activity.scope, @date_to, @date_from, @with_subprojects, @author, events.first, User.current, current_language])
-
respond_to do |format|
-
format.html {
-
@events_by_day = events.group_by {|event| User.current.time_to_date(event.event_datetime)}
-
render :layout => false if request.xhr?
-
}
-
format.atom {
-
title = l(:label_activity)
-
if @author
-
title = @author.name
-
elsif @activity.scope.size == 1
-
title = l("label_#{@activity.scope.first.singularize}_plural")
-
end
-
render_feed(events, :title => "#{@project || Setting.app_title}: #{title}")
-
}
-
end
-
end
-
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
private
-
-
# TODO: refactor, duplicated in projects_controller
-
1
def find_optional_project
-
return true unless params[:id]
-
@project = Project.find(params[:id])
-
authorize
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AdminController < ApplicationController
-
1
layout 'admin'
-
1
menu_item :projects, :only => :projects
-
1
menu_item :plugins, :only => :plugins
-
1
menu_item :info, :only => :info
-
-
1
before_filter :require_admin
-
1
helper :sort
-
1
include SortHelper
-
-
1
def index
-
@no_configuration_data = Redmine::DefaultData::Loader::no_data?
-
end
-
-
1
def projects
-
@status = params[:status] || 1
-
-
scope = Project.status(@status)
-
scope = scope.like(params[:name]) if params[:name].present?
-
-
@projects = scope.all(:order => 'lft')
-
-
render :action => "projects", :layout => false if request.xhr?
-
end
-
-
1
def plugins
-
@plugins = Redmine::Plugin.all
-
end
-
-
# Loads the default configuration
-
# (roles, trackers, statuses, workflow, enumerations)
-
1
def default_configuration
-
if request.post?
-
begin
-
Redmine::DefaultData::Loader::load(params[:lang])
-
flash[:notice] = l(:notice_default_data_loaded)
-
rescue Exception => e
-
flash[:error] = l(:error_can_t_load_default_data, e.message)
-
end
-
end
-
redirect_to :action => 'index'
-
end
-
-
1
def test_email
-
raise_delivery_errors = ActionMailer::Base.raise_delivery_errors
-
# Force ActionMailer to raise delivery errors so we can catch it
-
ActionMailer::Base.raise_delivery_errors = true
-
begin
-
@test = Mailer.test_email(User.current).deliver
-
flash[:notice] = l(:notice_email_sent, User.current.mail)
-
rescue Exception => e
-
flash[:error] = l(:notice_email_error, e.message)
-
end
-
ActionMailer::Base.raise_delivery_errors = raise_delivery_errors
-
redirect_to :controller => 'settings', :action => 'edit', :tab => 'notifications'
-
end
-
-
1
def info
-
@db_adapter_name = ActiveRecord::Base.connection.adapter_name
-
@checklist = [
-
[:text_default_administrator_account_changed, User.default_admin_account_changed?],
-
[:text_file_repository_writable, File.writable?(Attachment.storage_path)],
-
[:text_plugin_assets_writable, File.writable?(Redmine::Plugin.public_directory)],
-
[:text_rmagick_available, Object.const_defined?(:Magick)]
-
]
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'uri'
-
1
require 'cgi'
-
-
1
class Unauthorized < Exception; end
-
-
1
class ApplicationController < ActionController::Base
-
1
include Redmine::I18n
-
-
1
class_attribute :accept_api_auth_actions
-
1
class_attribute :accept_rss_auth_actions
-
1
class_attribute :model_object
-
-
1
layout 'base'
-
-
1
protect_from_forgery
-
1
def handle_unverified_request
-
super
-
cookies.delete(:autologin)
-
end
-
-
1
before_filter :user_setup, :check_if_login_required, :set_localization
-
-
1
rescue_from ActionController::InvalidAuthenticityToken, :with => :invalid_authenticity_token
-
1
rescue_from ::Unauthorized, :with => :deny_access
-
-
1
include Redmine::Search::Controller
-
1
include Redmine::MenuManager::MenuController
-
1
helper Redmine::MenuManager::MenuHelper
-
-
1
def user_setup
-
# Check the settings cache for each request
-
854
Setting.check_cache
-
# Find the current user
-
854
User.current = find_current_user
-
end
-
-
# Returns the current user or nil if no user is logged in
-
# and starts a session if needed
-
1
def find_current_user
-
854
if session[:user_id]
-
# existing session
-
569
(User.active.find(session[:user_id]) rescue nil)
-
285
elsif cookies[:autologin] && Setting.autologin?
-
# auto-login feature starts a new session
-
user = User.try_to_autologin(cookies[:autologin])
-
session[:user_id] = user.id if user
-
user
-
285
elsif params[:format] == 'atom' && params[:key] && request.get? && accept_rss_auth?
-
# RSS key authentication does not start a session
-
User.find_by_rss_key(params[:key])
-
285
elsif Setting.rest_api_enabled? && accept_api_auth?
-
2
if (key = api_key_from_request)
-
# Use API key
-
2
User.find_by_api_key(key)
-
else
-
# HTTP Basic, either username/password or API key/random
-
authenticate_with_http_basic do |username, password|
-
User.try_to_login(username, password) || User.find_by_api_key(username)
-
end
-
end
-
end
-
end
-
-
# Sets the logged in user
-
1
def logged_user=(user)
-
127
reset_session
-
127
if user && user.is_a?(User)
-
123
User.current = user
-
123
session[:user_id] = user.id
-
else
-
4
User.current = User.anonymous
-
end
-
end
-
-
# Logs out current user
-
1
def logout_user
-
127
if User.current.logged?
-
4
cookies.delete :autologin
-
4
Token.delete_all(["user_id = ? AND action = ?", User.current.id, 'autologin'])
-
4
self.logged_user = nil
-
end
-
end
-
-
# check if login is globally required to access the application
-
1
def check_if_login_required
-
# no check needed if user is already logged in
-
604
return true if User.current.logged?
-
38
require_login if Setting.login_required?
-
end
-
-
1
def set_localization
-
854
lang = nil
-
854
if User.current.logged?
-
570
lang = find_language(User.current.language)
-
end
-
854
if lang.nil? && request.env['HTTP_ACCEPT_LANGUAGE']
-
79
accept_lang = parse_qvalues(request.env['HTTP_ACCEPT_LANGUAGE']).first
-
79
if !accept_lang.blank?
-
79
accept_lang = accept_lang.downcase
-
79
lang = find_language(accept_lang) || find_language(accept_lang.split('-').first)
-
end
-
end
-
854
lang ||= Setting.default_language
-
854
set_language_if_valid(lang)
-
end
-
-
1
def require_login
-
129
if !User.current.logged?
-
# Extract only the basic url parameters on non-GET requests
-
6
if request.get?
-
1
url = url_for(params)
-
else
-
5
url = url_for(:controller => params[:controller], :action => params[:action], :id => params[:id], :project_id => params[:project_id])
-
end
-
6
respond_to do |format|
-
9
format.html { redirect_to :controller => "account", :action => "login", :back_url => url }
-
6
format.atom { redirect_to :controller => "account", :action => "login", :back_url => url }
-
7
format.xml { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
-
8
format.js { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
-
6
format.json { head :unauthorized, 'WWW-Authenticate' => 'Basic realm="Redmine API"' }
-
end
-
6
return false
-
end
-
123
true
-
end
-
-
1
def require_admin
-
return unless require_login
-
if !User.current.admin?
-
render_403
-
return false
-
end
-
true
-
end
-
-
1
def deny_access
-
9
User.current.logged? ? render_403 : require_login
-
end
-
-
# Authorize the user for the requested action
-
1
def authorize(ctrl = params[:controller], action = params[:action], global = false)
-
367
allowed = User.current.allowed_to?({:controller => ctrl, :action => action}, @project || @projects, :global => global)
-
367
if allowed
-
358
true
-
else
-
9
if @project && @project.archived?
-
render_403 :message => :notice_not_authorized_archived_project
-
else
-
9
deny_access
-
end
-
end
-
end
-
-
# Authorize the user for the requested action outside a project
-
1
def authorize_global(ctrl = params[:controller], action = params[:action], global = true)
-
7
authorize(ctrl, action, global)
-
end
-
-
# Find project of id params[:id]
-
1
def find_project
-
67
@project = Project.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Find project of id params[:project_id]
-
1
def find_project_by_project_id
-
@project = Project.find(params[:project_id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Find a project based on params[:project_id]
-
# TODO: some subclasses override this, see about merging their logic
-
1
def find_optional_project
-
14
@project = Project.find(params[:project_id]) unless params[:project_id].blank?
-
14
allowed = User.current.allowed_to?({:controller => params[:controller], :action => params[:action]}, @project, :global => true)
-
14
allowed ? true : deny_access
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Finds and sets @project based on @object.project
-
1
def find_project_from_association
-
render_404 unless @object.present?
-
-
@project = @object.project
-
end
-
-
1
def find_model_object
-
model = self.class.model_object
-
if model
-
@object = model.find(params[:id])
-
self.instance_variable_set('@' + controller_name.singularize, @object) if @object
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def self.model_object(model)
-
6
self.model_object = model
-
end
-
-
# Filter for bulk issue operations
-
1
def find_issues
-
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
-
raise ActiveRecord::RecordNotFound if @issues.empty?
-
if @issues.detect {|issue| !issue.visible?}
-
deny_access
-
return
-
end
-
@projects = @issues.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# make sure that the user is a member of the project (or admin) if project is private
-
# used as a before_filter for actions that do not require any particular permission on the project
-
1
def check_project_privacy
-
if @project && @project.active?
-
if @project.visible?
-
true
-
else
-
deny_access
-
end
-
else
-
@project = nil
-
render_404
-
false
-
end
-
end
-
-
1
def back_url
-
params[:back_url] || request.env['HTTP_REFERER']
-
end
-
-
1
def redirect_back_or_default(default)
-
125
back_url = CGI.unescape(params[:back_url].to_s)
-
125
if !back_url.blank?
-
2
begin
-
2
uri = URI.parse(back_url)
-
# do not redirect user to another host or to the login or register page
-
2
if (uri.relative? || (uri.host == request.host)) && !uri.path.match(%r{/(login|account/register)})
-
2
redirect_to(back_url)
-
2
return
-
end
-
rescue URI::InvalidURIError
-
# redirect to default
-
end
-
end
-
123
redirect_to default
-
123
false
-
end
-
-
# Redirects to the request referer if present, redirects to args or call block otherwise.
-
1
def redirect_to_referer_or(*args, &block)
-
redirect_to :back
-
rescue ::ActionController::RedirectBackError
-
if args.any?
-
redirect_to *args
-
elsif block_given?
-
block.call
-
else
-
raise "#redirect_to_referer_or takes arguments or a block"
-
end
-
end
-
-
1
def render_403(options={})
-
3
@project = nil
-
3
render_error({:message => :notice_not_authorized, :status => 403}.merge(options))
-
3
return false
-
end
-
-
1
def render_404(options={})
-
render_error({:message => :notice_file_not_found, :status => 404}.merge(options))
-
return false
-
end
-
-
# Renders an error response
-
1
def render_error(arg)
-
3
arg = {:message => arg} unless arg.is_a?(Hash)
-
-
3
@message = arg[:message]
-
3
@message = l(@message) if @message.is_a?(Symbol)
-
3
@status = arg[:status] || 500
-
-
3
respond_to do |format|
-
3
format.html {
-
render :template => 'common/error', :layout => use_layout, :status => @status
-
}
-
3
format.atom { head @status }
-
3
format.xml { head @status }
-
3
format.js { head @status }
-
6
format.json { head @status }
-
end
-
end
-
-
# Filter for actions that provide an API response
-
# but have no HTML representation for non admin users
-
1
def require_admin_or_api_request
-
1
return true if api_request?
-
if User.current.admin?
-
true
-
elsif User.current.logged?
-
render_error(:status => 406)
-
else
-
deny_access
-
end
-
end
-
-
# Picks which layout to use based on the request
-
#
-
# @return [boolean, string] name of the layout to use or false for no layout
-
1
def use_layout
-
request.xhr? ? false : 'base'
-
end
-
-
1
def invalid_authenticity_token
-
if api_request?
-
logger.error "Form authenticity token is missing or is invalid. API calls must include a proper Content-type header (text/xml or text/json)."
-
end
-
render_error "Invalid form authenticity token."
-
end
-
-
1
def render_feed(items, options={})
-
@items = items || []
-
@items.sort! {|x,y| y.event_datetime <=> x.event_datetime }
-
@items = @items.slice(0, Setting.feeds_limit.to_i)
-
@title = options[:title] || Setting.app_title
-
render :template => "common/feed.atom", :layout => false,
-
:content_type => 'application/atom+xml'
-
end
-
-
1
def self.accept_rss_auth(*actions)
-
8
if actions.any?
-
8
self.accept_rss_auth_actions = actions
-
else
-
self.accept_rss_auth_actions || []
-
end
-
end
-
-
1
def accept_rss_auth?(action=action_name)
-
self.class.accept_rss_auth.include?(action.to_sym)
-
end
-
-
1
def self.accept_api_auth(*actions)
-
19
if actions.any?
-
15
self.accept_api_auth_actions = actions
-
else
-
4
self.accept_api_auth_actions || []
-
end
-
end
-
-
1
def accept_api_auth?(action=action_name)
-
4
self.class.accept_api_auth.include?(action.to_sym)
-
end
-
-
# Returns the number of objects that should be displayed
-
# on the paginated list
-
1
def per_page_option
-
18
per_page = nil
-
18
if params[:per_page] && Setting.per_page_options_array.include?(params[:per_page].to_s.to_i)
-
per_page = params[:per_page].to_s.to_i
-
session[:per_page] = per_page
-
elsif session[:per_page]
-
per_page = session[:per_page]
-
else
-
18
per_page = Setting.per_page_options_array.first || 25
-
end
-
18
per_page
-
end
-
-
# Returns offset and limit used to retrieve objects
-
# for an API response based on offset, limit and page parameters
-
1
def api_offset_and_limit(options=params)
-
if options[:offset].present?
-
offset = options[:offset].to_i
-
if offset < 0
-
offset = 0
-
end
-
end
-
limit = options[:limit].to_i
-
if limit < 1
-
limit = 25
-
elsif limit > 100
-
limit = 100
-
end
-
if offset.nil? && options[:page].present?
-
offset = (options[:page].to_i - 1) * limit
-
offset = 0 if offset < 0
-
end
-
offset ||= 0
-
-
[offset, limit]
-
end
-
-
# qvalues http header parser
-
# code taken from webrick
-
1
def parse_qvalues(value)
-
79
tmp = []
-
79
if value
-
79
parts = value.split(/,\s*/)
-
79
parts.each {|part|
-
237
if m = %r{^([^\s,]+?)(?:;\s*q=(\d+(?:\.\d+)?))?$}.match(part)
-
237
val = m[1]
-
237
q = (m[2] or 1).to_f
-
237
tmp.push([val, q])
-
end
-
}
-
316
tmp = tmp.sort_by{|val, q| -q}
-
316
tmp.collect!{|val, q| val}
-
end
-
79
return tmp
-
rescue
-
nil
-
end
-
-
# Returns a string that can be used as filename value in Content-Disposition header
-
1
def filename_for_content_disposition(name)
-
request.env['HTTP_USER_AGENT'] =~ %r{MSIE} ? ERB::Util.url_encode(name) : name
-
end
-
-
1
def api_request?
-
1181
%w(xml json).include? params[:format]
-
end
-
-
# Returns the API key present in the request
-
1
def api_key_from_request
-
2
if params[:key].present?
-
2
params[:key]
-
elsif request.headers["X-Redmine-API-Key"].present?
-
request.headers["X-Redmine-API-Key"]
-
end
-
end
-
-
# Renders a warning flash if obj has unsaved attachments
-
1
def render_attachment_warning_if_needed(obj)
-
2
flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
-
end
-
-
# Sets the `flash` notice or error based the number of issues that did not save
-
#
-
# @param [Array, Issue] issues all of the saved and unsaved Issues
-
# @param [Array, Integer] unsaved_issue_ids the issue ids that were not saved
-
1
def set_flash_from_bulk_issue_save(issues, unsaved_issue_ids)
-
if unsaved_issue_ids.empty?
-
flash[:notice] = l(:notice_successful_update) unless issues.empty?
-
else
-
flash[:error] = l(:notice_failed_to_save_issues,
-
:count => unsaved_issue_ids.size,
-
:total => issues.size,
-
:ids => '#' + unsaved_issue_ids.join(', #'))
-
end
-
end
-
-
# Rescues an invalid query statement. Just in case...
-
1
def query_statement_invalid(exception)
-
logger.error "Query::StatementInvalid: #{exception.message}" if logger
-
session.delete(:query)
-
sort_clear if respond_to?(:sort_clear)
-
render_error "An error occurred while executing the query and has been logged. Please report this error to your Redmine administrator."
-
end
-
-
# Renders API response on validation failure
-
1
def render_validation_errors(objects)
-
if objects.is_a?(Array)
-
@error_messages = objects.map {|object| object.errors.full_messages}.flatten
-
else
-
@error_messages = objects.errors.full_messages
-
end
-
render :template => 'common/error_messages.api', :status => :unprocessable_entity, :layout => nil
-
end
-
-
# Overrides #_include_layout? so that #render with no arguments
-
# doesn't use the layout for api requests
-
1
def _include_layout?(*args)
-
1166
api_request? ? false : super
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AttachmentsController < ApplicationController
-
1
before_filter :find_project, :except => :upload
-
1
before_filter :file_readable, :read_authorize, :only => [:show, :download]
-
1
before_filter :delete_authorize, :only => :destroy
-
1
before_filter :authorize_global, :only => :upload
-
-
1
accept_api_auth :show, :download, :upload
-
-
1
def show
-
respond_to do |format|
-
format.html {
-
if @attachment.is_diff?
-
@diff = File.new(@attachment.diskfile, "rb").read
-
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
-
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
-
# Save diff type as user preference
-
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
-
User.current.pref[:diff_type] = @diff_type
-
User.current.preference.save
-
end
-
render :action => 'diff'
-
elsif @attachment.is_text? && @attachment.filesize <= Setting.file_max_size_displayed.to_i.kilobyte
-
@content = File.new(@attachment.diskfile, "rb").read
-
render :action => 'file'
-
else
-
download
-
end
-
}
-
format.api
-
end
-
end
-
-
1
def download
-
if @attachment.container.is_a?(Version) || @attachment.container.is_a?(Project)
-
@attachment.increment_download
-
end
-
-
# images are sent inline
-
send_file @attachment.diskfile, :filename => filename_for_content_disposition(@attachment.filename),
-
:type => detect_content_type(@attachment),
-
:disposition => (@attachment.image? ? 'inline' : 'attachment')
-
-
end
-
-
1
def upload
-
# Make sure that API users get used to set this content type
-
# as it won't trigger Rails' automatic parsing of the request body for parameters
-
unless request.content_type == 'application/octet-stream'
-
render :nothing => true, :status => 406
-
return
-
end
-
-
@attachment = Attachment.new(:file => request.raw_post)
-
@attachment.author = User.current
-
@attachment.filename = Redmine::Utils.random_hex(16)
-
-
if @attachment.save
-
respond_to do |format|
-
format.api { render :action => 'upload', :status => :created }
-
end
-
else
-
respond_to do |format|
-
format.api { render_validation_errors(@attachment) }
-
end
-
end
-
end
-
-
1
def destroy
-
if @attachment.container.respond_to?(:init_journal)
-
@attachment.container.init_journal(User.current)
-
end
-
# Make sure association callbacks are called
-
@attachment.container.attachments.delete(@attachment)
-
redirect_to_referer_or project_path(@project)
-
end
-
-
1
private
-
1
def find_project
-
@attachment = Attachment.find(params[:id])
-
# Show 404 if the filename in the url is wrong
-
raise ActiveRecord::RecordNotFound if params[:filename] && params[:filename] != @attachment.filename
-
@project = @attachment.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Checks that the file exists and is readable
-
1
def file_readable
-
@attachment.readable? ? true : render_404
-
end
-
-
1
def read_authorize
-
@attachment.visible? ? true : deny_access
-
end
-
-
1
def delete_authorize
-
@attachment.deletable? ? true : deny_access
-
end
-
-
1
def detect_content_type(attachment)
-
content_type = attachment.content_type
-
if content_type.blank?
-
content_type = Redmine::MimeType.of(attachment.filename)
-
end
-
content_type.to_s
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AuthSourcesController < ApplicationController
-
1
layout 'admin'
-
1
menu_item :ldap_authentication
-
-
1
before_filter :require_admin
-
-
1
def index
-
@auth_source_pages, @auth_sources = paginate AuthSource, :per_page => 10
-
end
-
-
1
def new
-
klass_name = params[:type] || 'AuthSourceLdap'
-
@auth_source = AuthSource.new_subclass_instance(klass_name, params[:auth_source])
-
end
-
-
1
def create
-
@auth_source = AuthSource.new_subclass_instance(params[:type], params[:auth_source])
-
if @auth_source.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
@auth_source = AuthSource.find(params[:id])
-
end
-
-
1
def update
-
@auth_source = AuthSource.find(params[:id])
-
if @auth_source.update_attributes(params[:auth_source])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def test_connection
-
@auth_source = AuthSource.find(params[:id])
-
begin
-
@auth_source.test_connection
-
flash[:notice] = l(:notice_successful_connection)
-
rescue Exception => e
-
flash[:error] = l(:error_unable_to_connect, e.message)
-
end
-
redirect_to :action => 'index'
-
end
-
-
1
def destroy
-
@auth_source = AuthSource.find(params[:id])
-
unless @auth_source.users.find(:first)
-
@auth_source.destroy
-
flash[:notice] = l(:notice_successful_delete)
-
end
-
redirect_to :action => 'index'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class AutoCompletesController < ApplicationController
-
1
before_filter :find_project
-
-
1
def issues
-
@issues = []
-
q = params[:q].to_s
-
query = (params[:scope] == "all" && Setting.cross_project_issue_relations?) ? Issue : @project.issues
-
if q.match(/^\d+$/)
-
@issues << query.visible.find_by_id(q.to_i)
-
end
-
unless q.blank?
-
@issues += query.visible.find(:all, :conditions => ["LOWER(#{Issue.table_name}.subject) LIKE ?", "%#{q.downcase}%"], :limit => 10)
-
end
-
@issues.compact!
-
render :layout => false
-
end
-
-
1
private
-
-
1
def find_project
-
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
-
@project = Project.find(project_id)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class BoardsController < ApplicationController
-
1
default_search_scope :messages
-
1
before_filter :find_project_by_project_id, :find_board_if_available, :authorize
-
1
accept_rss_auth :index, :show
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :watchers
-
-
1
def index
-
@boards = @project.boards
-
# show the board if there is only one
-
if @boards.size == 1
-
@board = @boards.first
-
show
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html {
-
sort_init 'updated_on', 'desc'
-
sort_update 'created_on' => "#{Message.table_name}.created_on",
-
'replies' => "#{Message.table_name}.replies_count",
-
'updated_on' => "#{Message.table_name}.updated_on"
-
-
@topic_count = @board.topics.count
-
@topic_pages = Paginator.new self, @topic_count, per_page_option, params['page']
-
@topics = @board.topics.find :all, :order => ["#{Message.table_name}.sticky DESC", sort_clause].compact.join(', '),
-
:include => [:author, {:last_reply => :author}],
-
:limit => @topic_pages.items_per_page,
-
:offset => @topic_pages.current.offset
-
@message = Message.new(:board => @board)
-
render :action => 'show', :layout => !request.xhr?
-
}
-
format.atom {
-
@messages = @board.messages.find :all, :order => 'created_on DESC',
-
:include => [:author, :board],
-
:limit => Setting.feeds_limit.to_i
-
render_feed(@messages, :title => "#{@project}: #{@board}")
-
}
-
end
-
end
-
-
1
def new
-
@board = @project.boards.build
-
@board.safe_attributes = params[:board]
-
end
-
-
1
def create
-
@board = @project.boards.build
-
@board.safe_attributes = params[:board]
-
if @board.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to_settings_in_projects
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@board.safe_attributes = params[:board]
-
if @board.save
-
redirect_to_settings_in_projects
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@board.destroy
-
redirect_to_settings_in_projects
-
end
-
-
1
private
-
1
def redirect_to_settings_in_projects
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'boards'
-
end
-
-
1
def find_board_if_available
-
@board = @project.boards.find(params[:id]) if params[:id]
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CalendarsController < ApplicationController
-
1
menu_item :calendar
-
1
before_filter :find_optional_project
-
-
1
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
-
-
1
helper :issues
-
1
helper :projects
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :sort
-
1
include SortHelper
-
-
1
def show
-
if params[:year] and params[:year].to_i > 1900
-
@year = params[:year].to_i
-
if params[:month] and params[:month].to_i > 0 and params[:month].to_i < 13
-
@month = params[:month].to_i
-
end
-
end
-
@year ||= Date.today.year
-
@month ||= Date.today.month
-
-
@calendar = Redmine::Helpers::Calendar.new(Date.civil(@year, @month, 1), current_language, :month)
-
retrieve_query
-
@query.group_by = nil
-
if @query.valid?
-
events = []
-
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
-
:conditions => ["((start_date BETWEEN ? AND ?) OR (due_date BETWEEN ? AND ?))", @calendar.startdt, @calendar.enddt, @calendar.startdt, @calendar.enddt]
-
)
-
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @calendar.startdt, @calendar.enddt])
-
-
@calendar.events = events
-
end
-
-
render :action => 'show', :layout => false if request.xhr?
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CommentsController < ApplicationController
-
1
default_search_scope :news
-
1
model_object News
-
1
before_filter :find_model_object
-
1
before_filter :find_project_from_association
-
1
before_filter :authorize
-
-
1
def create
-
raise Unauthorized unless @news.commentable?
-
-
@comment = Comment.new
-
@comment.safe_attributes = params[:comment]
-
@comment.author = User.current
-
if @news.comments << @comment
-
flash[:notice] = l(:label_comment_added)
-
end
-
-
redirect_to :controller => 'news', :action => 'show', :id => @news
-
end
-
-
1
def destroy
-
@news.comments.find(params[:comment_id]).destroy
-
redirect_to :controller => 'news', :action => 'show', :id => @news
-
end
-
-
1
private
-
-
# ApplicationController's find_model_object sets it based on the controller
-
# name so it needs to be overriden and set to @news instead
-
1
def find_model_object
-
super
-
@news = @object
-
@comment = nil
-
@news
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ContextMenusController < ApplicationController
-
1
helper :watchers
-
1
helper :issues
-
-
1
def issues
-
@issues = Issue.visible.all(:conditions => {:id => params[:ids]}, :include => :project)
-
if (@issues.size == 1)
-
@issue = @issues.first
-
end
-
-
@allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
-
@projects = @issues.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
-
@can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
-
:log_time => (@project && User.current.allowed_to?(:log_time, @project)),
-
:update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
-
:move => (@project && User.current.allowed_to?(:move_issues, @project)),
-
:copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
-
:delete => User.current.allowed_to?(:delete_issues, @projects)
-
}
-
if @project
-
if @issue
-
@assignables = @issue.assignable_users
-
else
-
@assignables = @project.assignable_users
-
end
-
@trackers = @project.trackers
-
else
-
#when multiple projects, we only keep the intersection of each set
-
@assignables = @projects.map(&:assignable_users).reduce(:&)
-
@trackers = @projects.map(&:trackers).reduce(:&)
-
end
-
-
@priorities = IssuePriority.active.reverse
-
@back = back_url
-
-
@options_by_custom_field = {}
-
if @can[:edit]
-
custom_fields = @issues.map(&:available_custom_fields).reduce(:&).select do |f|
-
%w(bool list user version).include?(f.field_format) && !f.multiple?
-
end
-
custom_fields.each do |field|
-
values = field.possible_values_options(@projects)
-
if values.any?
-
@options_by_custom_field[field] = values
-
end
-
end
-
end
-
-
render :layout => false
-
end
-
-
1
def time_entries
-
@time_entries = TimeEntry.all(
-
:conditions => {:id => params[:ids]}, :include => :project)
-
@projects = @time_entries.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
@activities = TimeEntryActivity.shared.active
-
@can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
-
:delete => User.current.allowed_to?(:edit_time_entries, @projects)
-
}
-
@back = back_url
-
render :layout => false
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomFieldsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin
-
1
before_filter :build_new_custom_field, :only => [:new, :create]
-
1
before_filter :find_custom_field, :only => [:edit, :update, :destroy]
-
-
1
def index
-
@custom_fields_by_type = CustomField.find(:all).group_by {|f| f.class.name }
-
@tab = params[:tab] || 'IssueCustomField'
-
end
-
-
1
def new
-
end
-
-
1
def create
-
if request.post? and @custom_field.save
-
flash[:notice] = l(:notice_successful_create)
-
call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
-
redirect_to :action => 'index', :tab => @custom_field.class.name
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? and @custom_field.update_attributes(params[:custom_field])
-
flash[:notice] = l(:notice_successful_update)
-
call_hook(:controller_custom_fields_edit_after_save, :params => params, :custom_field => @custom_field)
-
redirect_to :action => 'index', :tab => @custom_field.class.name
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@custom_field.destroy
-
redirect_to :action => 'index', :tab => @custom_field.class.name
-
rescue
-
flash[:error] = l(:error_can_not_delete_custom_field)
-
redirect_to :action => 'index'
-
end
-
-
1
private
-
-
1
def build_new_custom_field
-
@custom_field = CustomField.new_subclass_instance(params[:type], params[:custom_field])
-
if @custom_field.nil?
-
render_404
-
end
-
end
-
-
1
def find_custom_field
-
@custom_field = CustomField.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentsController < ApplicationController
-
1
default_search_scope :documents
-
1
model_object Document
-
1
before_filter :find_project_by_project_id, :only => [:index, :new, :create]
-
1
before_filter :find_model_object, :except => [:index, :new, :create]
-
1
before_filter :find_project_from_association, :except => [:index, :new, :create]
-
1
before_filter :authorize
-
-
1
helper :attachments
-
-
1
def index
-
@sort_by = %w(category date title author).include?(params[:sort_by]) ? params[:sort_by] : 'category'
-
documents = @project.documents.find :all, :include => [:attachments, :category]
-
case @sort_by
-
when 'date'
-
@grouped = documents.group_by {|d| d.updated_on.to_date }
-
when 'title'
-
@grouped = documents.group_by {|d| d.title.first.upcase}
-
when 'author'
-
@grouped = documents.select{|d| d.attachments.any?}.group_by {|d| d.attachments.last.author}
-
else
-
@grouped = documents.group_by(&:category)
-
end
-
@document = @project.documents.build
-
render :layout => false if request.xhr?
-
end
-
-
1
def show
-
@attachments = @document.attachments.find(:all, :order => "created_on DESC")
-
end
-
-
1
def new
-
@document = @project.documents.build
-
@document.safe_attributes = params[:document]
-
end
-
-
1
def create
-
@document = @project.documents.build
-
@document.safe_attributes = params[:document]
-
@document.save_attachments(params[:attachments])
-
if @document.save
-
render_attachment_warning_if_needed(@document)
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index', :project_id => @project
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@document.safe_attributes = params[:document]
-
if request.put? and @document.save
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'show', :id => @document
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@document.destroy if request.delete?
-
redirect_to :controller => 'documents', :action => 'index', :project_id => @project
-
end
-
-
1
def add_attachment
-
attachments = Attachment.attach_files(@document, params[:attachments])
-
render_attachment_warning_if_needed(@document)
-
-
if attachments.present? && attachments[:files].present? && Setting.notified_events.include?('document_added')
-
Mailer.attachments_added(attachments[:files]).deliver
-
end
-
redirect_to :action => 'show', :id => @document
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class EnumerationsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin
-
1
before_filter :build_new_enumeration, :only => [:new, :create]
-
1
before_filter :find_enumeration, :only => [:edit, :update, :destroy]
-
-
1
helper :custom_fields
-
-
1
def index
-
end
-
-
1
def new
-
end
-
-
1
def create
-
if request.post? && @enumeration.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index', :type => @enumeration.type
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? && @enumeration.update_attributes(params[:enumeration])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index', :type => @enumeration.type
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
if !@enumeration.in_use?
-
# No associated objects
-
@enumeration.destroy
-
redirect_to :action => 'index'
-
return
-
elsif params[:reassign_to_id]
-
if reassign_to = @enumeration.class.find_by_id(params[:reassign_to_id])
-
@enumeration.destroy(reassign_to)
-
redirect_to :action => 'index'
-
return
-
end
-
end
-
@enumerations = @enumeration.class.all - [@enumeration]
-
end
-
-
1
private
-
-
1
def build_new_enumeration
-
class_name = params[:enumeration] && params[:enumeration][:type] || params[:type]
-
@enumeration = Enumeration.new_subclass_instance(class_name, params[:enumeration])
-
if @enumeration.nil?
-
render_404
-
end
-
end
-
-
1
def find_enumeration
-
@enumeration = Enumeration.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class FilesController < ApplicationController
-
1
menu_item :files
-
-
1
before_filter :find_project_by_project_id
-
1
before_filter :authorize
-
-
1
helper :sort
-
1
include SortHelper
-
-
1
def index
-
sort_init 'filename', 'asc'
-
sort_update 'filename' => "#{Attachment.table_name}.filename",
-
'created_on' => "#{Attachment.table_name}.created_on",
-
'size' => "#{Attachment.table_name}.filesize",
-
'downloads' => "#{Attachment.table_name}.downloads"
-
-
@containers = [ Project.find(@project.id, :include => :attachments, :order => sort_clause)]
-
@containers += @project.versions.find(:all, :include => :attachments, :order => sort_clause).sort.reverse
-
render :layout => !request.xhr?
-
end
-
-
1
def new
-
@versions = @project.versions.sort
-
end
-
-
1
def create
-
container = (params[:version_id].blank? ? @project : @project.versions.find_by_id(params[:version_id]))
-
attachments = Attachment.attach_files(container, params[:attachments])
-
render_attachment_warning_if_needed(container)
-
-
if !attachments.empty? && !attachments[:files].blank? && Setting.notified_events.include?('file_added')
-
Mailer.attachments_added(attachments[:files]).deliver
-
end
-
redirect_to project_files_path(@project)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class GanttsController < ApplicationController
-
1
menu_item :gantt
-
1
before_filter :find_optional_project
-
-
1
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
-
-
1
helper :gantt
-
1
helper :issues
-
1
helper :projects
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :sort
-
1
include SortHelper
-
1
include Redmine::Export::PDF
-
-
1
def show
-
@gantt = Redmine::Helpers::Gantt.new(params)
-
@gantt.project = @project
-
retrieve_query
-
@query.group_by = nil
-
@gantt.query = @query if @query.valid?
-
-
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
-
-
respond_to do |format|
-
format.html { render :action => "show", :layout => !request.xhr? }
-
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
-
format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class GroupsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin
-
-
1
helper :custom_fields
-
-
# GET /groups
-
# GET /groups.xml
-
1
def index
-
@groups = Group.find(:all, :order => 'lastname')
-
-
respond_to do |format|
-
format.html # index.html.erb
-
format.xml { render :xml => @groups }
-
end
-
end
-
-
# GET /groups/1
-
# GET /groups/1.xml
-
1
def show
-
@group = Group.find(params[:id])
-
-
respond_to do |format|
-
format.html # show.html.erb
-
format.xml { render :xml => @group }
-
end
-
end
-
-
# GET /groups/new
-
# GET /groups/new.xml
-
1
def new
-
@group = Group.new
-
-
respond_to do |format|
-
format.html # new.html.erb
-
format.xml { render :xml => @group }
-
end
-
end
-
-
# GET /groups/1/edit
-
1
def edit
-
@group = Group.find(params[:id], :include => :projects)
-
end
-
-
# POST /groups
-
# POST /groups.xml
-
1
def create
-
@group = Group.new(params[:group])
-
-
respond_to do |format|
-
if @group.save
-
format.html {
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to(params[:continue] ? new_group_path : groups_path)
-
}
-
format.xml { render :xml => @group, :status => :created, :location => @group }
-
else
-
format.html { render :action => "new" }
-
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
-
end
-
end
-
end
-
-
# PUT /groups/1
-
# PUT /groups/1.xml
-
1
def update
-
@group = Group.find(params[:id])
-
-
respond_to do |format|
-
if @group.update_attributes(params[:group])
-
flash[:notice] = l(:notice_successful_update)
-
format.html { redirect_to(groups_path) }
-
format.xml { head :ok }
-
else
-
format.html { render :action => "edit" }
-
format.xml { render :xml => @group.errors, :status => :unprocessable_entity }
-
end
-
end
-
end
-
-
# DELETE /groups/1
-
# DELETE /groups/1.xml
-
1
def destroy
-
@group = Group.find(params[:id])
-
@group.destroy
-
-
respond_to do |format|
-
format.html { redirect_to(groups_url) }
-
format.xml { head :ok }
-
end
-
end
-
-
1
def add_users
-
@group = Group.find(params[:id])
-
users = User.find_all_by_id(params[:user_ids])
-
@group.users << users if request.post?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
-
format.js {
-
render(:update) {|page|
-
page.replace_html "tab-content-users", :partial => 'groups/users'
-
users.each {|user| page.visual_effect(:highlight, "user-#{user.id}") }
-
}
-
}
-
end
-
end
-
-
1
def remove_user
-
@group = Group.find(params[:id])
-
@group.users.delete(User.find(params[:user_id])) if request.delete?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'users' }
-
format.js { render(:update) {|page| page.replace_html "tab-content-users", :partial => 'groups/users'} }
-
end
-
end
-
-
1
def autocomplete_for_user
-
@group = Group.find(params[:id])
-
@users = User.active.not_in_group(@group).like(params[:q]).all(:limit => 100)
-
render :layout => false
-
end
-
-
1
def edit_membership
-
@group = Group.find(params[:id])
-
@membership = Member.edit_membership(params[:membership_id], params[:membership], @group)
-
@membership.save if request.post?
-
respond_to do |format|
-
if @membership.valid?
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
-
format.js {
-
render(:update) {|page|
-
page.replace_html "tab-content-memberships", :partial => 'groups/memberships'
-
page.visual_effect(:highlight, "member-#{@membership.id}")
-
}
-
}
-
else
-
format.js {
-
render(:update) {|page|
-
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
-
}
-
}
-
end
-
end
-
end
-
-
1
def destroy_membership
-
@group = Group.find(params[:id])
-
Member.find(params[:membership_id]).destroy if request.post?
-
respond_to do |format|
-
format.html { redirect_to :controller => 'groups', :action => 'edit', :id => @group, :tab => 'memberships' }
-
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'groups/memberships'} }
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueCategoriesController < ApplicationController
-
1
menu_item :settings
-
1
model_object IssueCategory
-
1
before_filter :find_model_object, :except => [:index, :new, :create]
-
1
before_filter :find_project_from_association, :except => [:index, :new, :create]
-
1
before_filter :find_project_by_project_id, :only => [:index, :new, :create]
-
1
before_filter :authorize
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
def index
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
-
format.api { @categories = @project.issue_categories.all }
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project }
-
format.api
-
end
-
end
-
-
1
def new
-
@category = @project.issue_categories.build
-
@category.safe_attributes = params[:issue_category]
-
end
-
-
1
def create
-
@category = @project.issue_categories.build
-
@category.safe_attributes = params[:issue_category]
-
if @category.save
-
respond_to do |format|
-
format.html do
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
-
end
-
format.js do
-
# IE doesn't support the replace_html rjs method for select box options
-
render(:update) {|page| page.replace "issue_category_id",
-
content_tag('select', content_tag('option') + options_from_collection_for_select(@project.issue_categories, 'id', 'name', @category.id), :id => 'issue_category_id', :name => 'issue[category_id]')
-
}
-
end
-
format.api { render :action => 'show', :status => :created, :location => issue_category_path(@category) }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new'}
-
format.js do
-
render(:update) {|page| page.alert(@category.errors.full_messages.join('\n')) }
-
end
-
format.api { render_validation_errors(@category) }
-
end
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@category.safe_attributes = params[:issue_category]
-
if @category.save
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'categories', :id => @project
-
}
-
format.api { head :ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@category) }
-
end
-
end
-
end
-
-
1
def destroy
-
@issue_count = @category.issues.size
-
if @issue_count == 0 || params[:todo] || api_request?
-
reassign_to = nil
-
if params[:reassign_to_id] && (params[:todo] == 'reassign' || params[:todo].blank?)
-
reassign_to = @project.issue_categories.find_by_id(params[:reassign_to_id])
-
end
-
@category.destroy(reassign_to)
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'categories' }
-
format.api { head :ok }
-
end
-
return
-
end
-
@categories = @project.issue_categories - [@category]
-
end
-
-
1
private
-
# Wrap ApplicationController's find_model_object method to set
-
# @category instead of just @issue_category
-
1
def find_model_object
-
super
-
@category = @object
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueRelationsController < ApplicationController
-
1
before_filter :find_issue, :find_project_from_association, :authorize, :only => [:index, :create]
-
1
before_filter :find_relation, :except => [:index, :create]
-
-
1
accept_api_auth :index, :show, :create, :destroy
-
-
1
def index
-
@relations = @issue.relations
-
-
respond_to do |format|
-
format.html { render :nothing => true }
-
format.api
-
end
-
end
-
-
1
def show
-
raise Unauthorized unless @relation.visible?
-
-
respond_to do |format|
-
format.html { render :nothing => true }
-
format.api
-
end
-
end
-
-
1
def create
-
@relation = IssueRelation.new(params[:relation])
-
@relation.issue_from = @issue
-
if params[:relation] && m = params[:relation][:issue_to_id].to_s.strip.match(/^#?(\d+)$/)
-
@relation.issue_to = Issue.visible.find_by_id(m[1].to_i)
-
end
-
saved = @relation.save
-
-
respond_to do |format|
-
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @issue }
-
format.js do
-
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
-
render :update do |page|
-
page.replace_html "relations", :partial => 'issues/relations'
-
if @relation.errors.empty?
-
page << "$('relation_delay').value = ''"
-
page << "$('relation_issue_to_id').value = ''"
-
end
-
end
-
end
-
format.api {
-
if saved
-
render :action => 'show', :status => :created, :location => relation_url(@relation)
-
else
-
render_validation_errors(@relation)
-
end
-
}
-
end
-
end
-
-
1
def destroy
-
raise Unauthorized unless @relation.deletable?
-
@relation.destroy
-
-
respond_to do |format|
-
format.html { redirect_to issue_path } # TODO : does this really work since @issue is always nil? What is it useful to?
-
format.js { render(:update) {|page| page.remove "relation-#{@relation.id}"} }
-
format.api { head :ok }
-
end
-
end
-
-
1
private
-
1
def find_issue
-
@issue = @object = Issue.find(params[:issue_id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_relation
-
@relation = IssueRelation.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueStatusesController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :index
-
1
before_filter :require_admin_or_api_request, :only => :index
-
1
accept_api_auth :index
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@issue_status_pages, @issue_statuses = paginate :issue_statuses, :per_page => 25, :order => "position"
-
render :action => "index", :layout => false if request.xhr?
-
}
-
format.api {
-
@issue_statuses = IssueStatus.all(:order => 'position')
-
}
-
end
-
end
-
-
1
def new
-
@issue_status = IssueStatus.new
-
end
-
-
1
def create
-
@issue_status = IssueStatus.new(params[:issue_status])
-
if request.post? && @issue_status.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
@issue_status = IssueStatus.find(params[:id])
-
end
-
-
1
def update
-
@issue_status = IssueStatus.find(params[:id])
-
if request.put? && @issue_status.update_attributes(params[:issue_status])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
IssueStatus.find(params[:id]).destroy
-
redirect_to :action => 'index'
-
rescue
-
flash[:error] = l(:error_unable_delete_issue_status)
-
redirect_to :action => 'index'
-
end
-
-
1
def update_issue_done_ratio
-
if request.post? && IssueStatus.update_issue_done_ratios
-
flash[:notice] = l(:notice_issue_done_ratios_updated)
-
else
-
flash[:error] = l(:error_issue_done_ratios_not_updated)
-
end
-
redirect_to :action => 'index'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssuesController < ApplicationController
-
1
menu_item :new_issue, :only => [:new, :create]
-
1
default_search_scope :issues
-
-
1
before_filter :find_issue, :only => [:show, :edit, :update]
-
1
before_filter :find_issues, :only => [:bulk_edit, :bulk_update, :destroy]
-
1
before_filter :find_project, :only => [:new, :create]
-
1
before_filter :authorize, :except => [:index]
-
1
before_filter :find_optional_project, :only => [:index]
-
1
before_filter :check_for_default_issue_status, :only => [:new, :create]
-
1
before_filter :build_new_issue_from_params, :only => [:new, :create]
-
1
accept_rss_auth :index, :show
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
-
-
1
helper :journals
-
1
helper :projects
-
1
include ProjectsHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
1
helper :issue_relations
-
1
include IssueRelationsHelper
-
1
helper :watchers
-
1
include WatchersHelper
-
1
helper :attachments
-
1
include AttachmentsHelper
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :repositories
-
1
include RepositoriesHelper
-
1
helper :sort
-
1
include SortHelper
-
1
include IssuesHelper
-
1
helper :timelog
-
1
helper :gantt
-
1
include Redmine::Export::PDF
-
-
1
def index
-
14
retrieve_query
-
14
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
-
14
sort_update(@query.sortable_columns)
-
-
14
if @query.valid?
-
14
case params[:format]
-
when 'csv', 'pdf'
-
@limit = Setting.issues_export_limit.to_i
-
when 'atom'
-
@limit = Setting.feeds_limit.to_i
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
14
@limit = per_page_option
-
end
-
-
14
@issue_count = @query.issue_count
-
14
@issue_pages = Paginator.new self, @issue_count, @limit, params['page']
-
14
@offset ||= @issue_pages.current.offset
-
14
@issues = @query.issues(:include => [:assigned_to, :tracker, :priority, :category, :fixed_version],
-
:order => sort_clause,
-
:offset => @offset,
-
:limit => @limit)
-
14
@issue_count_by_group = @query.issue_count_by_group
-
-
14
respond_to do |format|
-
28
format.html { render :template => 'issues/index', :layout => !request.xhr? }
-
14
format.api {
-
Issue.load_relations(@issues) if include_in_api_response?('relations')
-
}
-
14
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
-
14
format.csv { send_data(issues_to_csv(@issues, @project, @query, params), :type => 'text/csv; header=present', :filename => 'export.csv') }
-
14
format.pdf { send_data(issues_to_pdf(@issues, @project, @query), :type => 'application/pdf', :filename => 'export.pdf') }
-
end
-
else
-
respond_to do |format|
-
format.html { render(:template => 'issues/index', :layout => !request.xhr?) }
-
format.any(:atom, :csv, :pdf) { render(:nothing => true) }
-
format.api { render_validation_errors(@query) }
-
end
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def show
-
2
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
-
2
@journals.each_with_index {|j,i| j.indice = i+1}
-
2
@journals.reverse! if User.current.wants_comments_in_reverse_order?
-
-
2
@changesets = @issue.changesets.visible.all
-
2
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
-
-
4
@relations = @issue.relations.select {|r| r.other_issue(@issue) && r.other_issue(@issue).visible? }
-
2
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
-
2
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
-
2
@priorities = IssuePriority.active
-
2
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
-
2
respond_to do |format|
-
2
format.html {
-
2
retrieve_previous_and_next_issue_ids
-
2
render :template => 'issues/show'
-
}
-
2
format.api
-
2
format.atom { render :template => 'journals/index', :layout => false, :content_type => 'application/atom+xml' }
-
2
format.pdf { send_data(issue_to_pdf(@issue), :type => 'application/pdf', :filename => "#{@project.identifier}-#{@issue.id}.pdf") }
-
end
-
end
-
-
# Add a new issue
-
# The new issue will be created from an existing one if copy_from parameter is given
-
1
def new
-
2
respond_to do |format|
-
4
format.html { render :action => 'new', :layout => !request.xhr? }
-
2
format.js {
-
render(:update) { |page|
-
if params[:project_change]
-
page.replace_html 'all_attributes', :partial => 'form'
-
else
-
page.replace_html 'attributes', :partial => 'attributes'
-
end
-
m = User.current.allowed_to?(:log_time, @issue.project) ? 'show' : 'hide'
-
page << "if ($('log_time')) {Element.#{m}('log_time');}"
-
}
-
}
-
end
-
end
-
-
1
def create
-
2
call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
-
2
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
-
2
if @issue.save
-
2
call_hook(:controller_issues_new_after_save, { :params => params, :issue => @issue})
-
2
respond_to do |format|
-
2
format.html {
-
2
render_attachment_warning_if_needed(@issue)
-
2
flash[:notice] = l(:notice_issue_successful_create, :id => "<a href='#{issue_path(@issue)}'>##{@issue.id}</a>")
-
redirect_to(params[:continue] ? { :action => 'new', :project_id => @issue.project, :issue => {:tracker_id => @issue.tracker, :parent_issue_id => @issue.parent_issue_id}.reject {|k,v| v.nil?} } :
-
2
{ :action => 'show', :id => @issue })
-
}
-
2
format.api { render :action => 'show', :status => :created, :location => issue_url(@issue) }
-
end
-
return
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@issue) }
-
end
-
end
-
end
-
-
1
def edit
-
return unless update_issue_from_params
-
-
respond_to do |format|
-
format.html { }
-
format.xml { }
-
end
-
end
-
-
1
def update
-
return unless update_issue_from_params
-
@issue.save_attachments(params[:attachments] || (params[:issue] && params[:issue][:uploads]))
-
saved = false
-
begin
-
saved = @issue.save_issue_with_child_records(params, @time_entry)
-
rescue ActiveRecord::StaleObjectError
-
@conflict = true
-
if params[:last_journal_id]
-
if params[:last_journal_id].present?
-
last_journal_id = params[:last_journal_id].to_i
-
@conflict_journals = @issue.journals.all(:conditions => ["#{Journal.table_name}.id > ?", last_journal_id])
-
else
-
@conflict_journals = @issue.journals.all
-
end
-
end
-
end
-
-
if saved
-
render_attachment_warning_if_needed(@issue)
-
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
-
-
respond_to do |format|
-
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
-
format.api { head :ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@issue) }
-
end
-
end
-
end
-
-
# Bulk edit/copy a set of issues
-
1
def bulk_edit
-
@issues.sort!
-
@copy = params[:copy].present?
-
@notes = params[:notes]
-
-
if User.current.allowed_to?(:move_issues, @projects)
-
@allowed_projects = Issue.allowed_target_projects_on_move
-
if params[:issue]
-
@target_project = @allowed_projects.detect {|p| p.id.to_s == params[:issue][:project_id].to_s}
-
if @target_project
-
target_projects = [@target_project]
-
end
-
end
-
end
-
target_projects ||= @projects
-
-
if @copy
-
@available_statuses = [IssueStatus.default]
-
else
-
@available_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
-
end
-
@custom_fields = target_projects.map{|p|p.all_issue_custom_fields}.reduce(:&)
-
@assignables = target_projects.map(&:assignable_users).reduce(:&)
-
@trackers = target_projects.map(&:trackers).reduce(:&)
-
@versions = target_projects.map {|p| p.shared_versions.open}.reduce(:&)
-
@categories = target_projects.map {|p| p.issue_categories}.reduce(:&)
-
if @copy
-
@attachments_present = @issues.detect {|i| i.attachments.any?}.present?
-
end
-
-
@safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
-
render :layout => false if request.xhr?
-
end
-
-
1
def bulk_update
-
@issues.sort!
-
@copy = params[:copy].present?
-
attributes = parse_params_for_bulk_issue_attributes(params)
-
-
unsaved_issue_ids = []
-
moved_issues = []
-
@issues.each do |issue|
-
issue.reload
-
if @copy
-
issue = issue.copy({}, :attachments => params[:copy_attachments].present?)
-
end
-
journal = issue.init_journal(User.current, params[:notes])
-
issue.safe_attributes = attributes
-
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
-
if issue.save
-
moved_issues << issue
-
else
-
# Keep unsaved issue ids to display them in flash error
-
unsaved_issue_ids << issue.id
-
end
-
end
-
set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
-
-
if params[:follow]
-
if @issues.size == 1 && moved_issues.size == 1
-
redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
-
elsif moved_issues.map(&:project).uniq.size == 1
-
redirect_to :controller => 'issues', :action => 'index', :project_id => moved_issues.map(&:project).first
-
end
-
else
-
redirect_back_or_default({:controller => 'issues', :action => 'index', :project_id => @project})
-
end
-
end
-
-
1
def destroy
-
@hours = TimeEntry.sum(:hours, :conditions => ['issue_id IN (?)', @issues]).to_f
-
if @hours > 0
-
case params[:todo]
-
when 'destroy'
-
# nothing to do
-
when 'nullify'
-
TimeEntry.update_all('issue_id = NULL', ['issue_id IN (?)', @issues])
-
when 'reassign'
-
reassign_to = @project.issues.find_by_id(params[:reassign_to_id])
-
if reassign_to.nil?
-
flash.now[:error] = l(:error_issue_not_found_in_project)
-
return
-
else
-
TimeEntry.update_all("issue_id = #{reassign_to.id}", ['issue_id IN (?)', @issues])
-
end
-
else
-
# display the destroy form if it's a user request
-
return unless api_request?
-
end
-
end
-
@issues.each do |issue|
-
begin
-
issue.reload.destroy
-
rescue ::ActiveRecord::RecordNotFound # raised by #reload if issue no longer exists
-
# nothing to do, issue was already deleted (eg. by a parent)
-
end
-
end
-
respond_to do |format|
-
format.html { redirect_back_or_default(:action => 'index', :project_id => @project) }
-
format.api { head :ok }
-
end
-
end
-
-
1
private
-
1
def find_issue
-
# Issue.visible.find(...) can not be used to redirect user to the login form
-
# if the issue actually exists but requires authentication
-
2
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
-
2
unless @issue.visible?
-
deny_access
-
return
-
end
-
2
@project = @issue.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_project
-
4
project_id = params[:project_id] || (params[:issue] && params[:issue][:project_id])
-
4
@project = Project.find(project_id)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def retrieve_previous_and_next_issue_ids
-
2
retrieve_query_from_session
-
2
if @query
-
sort_init(@query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
-
sort_update(@query.sortable_columns, 'issues_index_sort')
-
limit = 500
-
issue_ids = @query.issue_ids(:order => sort_clause, :limit => (limit + 1), :include => [:assigned_to, :tracker, :priority, :category, :fixed_version])
-
if (idx = issue_ids.index(@issue.id)) && idx < limit
-
if issue_ids.size < 500
-
@issue_position = idx + 1
-
@issue_count = issue_ids.size
-
end
-
@prev_issue_id = issue_ids[idx - 1] if idx > 0
-
@next_issue_id = issue_ids[idx + 1] if idx < (issue_ids.size - 1)
-
end
-
end
-
end
-
-
# Used by #edit and #update to set some common instance variables
-
# from the params
-
# TODO: Refactor, not everything in here is needed by #edit
-
1
def update_issue_from_params
-
@edit_allowed = User.current.allowed_to?(:edit_issues, @project)
-
@time_entry = TimeEntry.new(:issue => @issue, :project => @issue.project)
-
@time_entry.attributes = params[:time_entry]
-
-
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
-
@issue.init_journal(User.current, @notes)
-
-
issue_attributes = params[:issue]
-
if issue_attributes && params[:conflict_resolution]
-
case params[:conflict_resolution]
-
when 'overwrite'
-
issue_attributes = issue_attributes.dup
-
issue_attributes.delete(:lock_version)
-
when 'add_notes'
-
issue_attributes = {}
-
when 'cancel'
-
redirect_to issue_path(@issue)
-
return false
-
end
-
end
-
@issue.safe_attributes = issue_attributes
-
@priorities = IssuePriority.active
-
@allowed_statuses = @issue.new_statuses_allowed_to(User.current)
-
true
-
end
-
-
# TODO: Refactor, lots of extra code in here
-
# TODO: Changing tracker on an existing issue should not trigger this
-
1
def build_new_issue_from_params
-
4
if params[:id].blank?
-
4
@issue = Issue.new
-
4
if params[:copy_from]
-
4
begin
-
4
@copy_from = Issue.visible.find(params[:copy_from])
-
4
@copy_attachments = params[:copy_attachments].present? || request.get?
-
4
@issue.copy_from(@copy_from, :attachments => @copy_attachments)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
return
-
end
-
end
-
4
@issue.project = @project
-
else
-
@issue = @project.issues.visible.find(params[:id])
-
end
-
-
4
@issue.project = @project
-
4
@issue.author = User.current
-
# Tracker must be set before custom field values
-
4
@issue.tracker ||= @project.trackers.find((params[:issue] && params[:issue][:tracker_id]) || params[:tracker_id] || :first)
-
4
if @issue.tracker.nil?
-
render_error l(:error_no_tracker_in_project)
-
return false
-
end
-
4
@issue.start_date ||= Date.today if Setting.default_issue_start_date_to_creation_date?
-
4
@issue.safe_attributes = params[:issue]
-
-
4
@priorities = IssuePriority.active
-
4
@allowed_statuses = @issue.new_statuses_allowed_to(User.current, true)
-
4
@available_watchers = (@issue.project.users.sort + @issue.watcher_users).uniq
-
end
-
-
1
def check_for_default_issue_status
-
4
if IssueStatus.default.nil?
-
render_error l(:error_no_default_issue_status)
-
return false
-
end
-
end
-
-
1
def parse_params_for_bulk_issue_attributes(params)
-
attributes = (params[:issue] || {}).reject {|k,v| v.blank?}
-
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
-
if custom = attributes[:custom_field_values]
-
custom.reject! {|k,v| v.blank?}
-
custom.keys.each do |k|
-
if custom[k].is_a?(Array)
-
custom[k] << '' if custom[k].delete('__none__')
-
else
-
custom[k] = '' if custom[k] == '__none__'
-
end
-
end
-
end
-
attributes
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class JournalsController < ApplicationController
-
1
before_filter :find_journal, :only => [:edit, :diff]
-
1
before_filter :find_issue, :only => [:new]
-
1
before_filter :find_optional_project, :only => [:index]
-
1
before_filter :authorize, :only => [:new, :edit, :diff]
-
1
accept_rss_auth :index
-
1
menu_item :issues
-
-
1
helper :issues
-
1
helper :custom_fields
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :sort
-
1
include SortHelper
-
-
1
def index
-
retrieve_query
-
sort_init 'id', 'desc'
-
sort_update(@query.sortable_columns)
-
-
if @query.valid?
-
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
-
:limit => 25)
-
end
-
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
-
render :layout => false, :content_type => 'application/atom+xml'
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def diff
-
@issue = @journal.issue
-
if params[:detail_id].present?
-
@detail = @journal.details.find_by_id(params[:detail_id])
-
else
-
@detail = @journal.details.detect {|d| d.prop_key == 'description'}
-
end
-
(render_404; return false) unless @issue && @detail
-
@diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
-
end
-
-
1
def new
-
journal = Journal.find(params[:journal_id]) if params[:journal_id]
-
if journal
-
user = journal.user
-
text = journal.notes
-
else
-
user = @issue.author
-
text = @issue.description
-
end
-
# Replaces pre blocks with [...]
-
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
-
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
-
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
-
-
render(:update) { |page|
-
page.<< "$('notes').value = \"#{escape_javascript content}\";"
-
page.show 'update'
-
page << "Form.Element.focus('notes');"
-
page << "Element.scrollTo('update');"
-
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
-
}
-
end
-
-
1
def edit
-
(render_403; return false) unless @journal.editable_by?(User.current)
-
if request.post?
-
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
-
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
-
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
-
respond_to do |format|
-
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
-
format.js { render :action => 'update' }
-
end
-
else
-
respond_to do |format|
-
format.html {
-
# TODO: implement non-JS journal update
-
render :nothing => true
-
}
-
format.js
-
end
-
end
-
end
-
-
1
private
-
-
1
def find_journal
-
@journal = Journal.find(params[:id])
-
@project = @journal.journalized.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# TODO: duplicated in IssuesController
-
1
def find_issue
-
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
-
@project = @issue.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MailHandlerController < ActionController::Base
-
1
before_filter :check_credential
-
-
# Submits an incoming email to MailHandler
-
1
def index
-
options = params.dup
-
email = options.delete(:email)
-
if MailHandler.receive(email, options)
-
render :nothing => true, :status => :created
-
else
-
render :nothing => true, :status => :unprocessable_entity
-
end
-
end
-
-
1
private
-
-
1
def check_credential
-
User.current = nil
-
unless Setting.mail_handler_api_enabled? && params[:key].to_s == Setting.mail_handler_api_key
-
render :text => 'Access denied. Incoming emails WS is disabled or key is invalid.', :status => 403
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MembersController < ApplicationController
-
1
model_object Member
-
1
before_filter :find_model_object, :except => [:index, :create, :autocomplete]
-
1
before_filter :find_project_from_association, :except => [:index, :create, :autocomplete]
-
1
before_filter :find_project_by_project_id, :only => [:index, :create, :autocomplete]
-
1
before_filter :authorize
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
def index
-
@offset, @limit = api_offset_and_limit
-
@member_count = @project.member_principals.count
-
@member_pages = Paginator.new self, @member_count, @limit, params['page']
-
@offset ||= @member_pages.current.offset
-
@members = @project.member_principals.all(
-
:order => "#{Member.table_name}.id",
-
:limit => @limit,
-
:offset => @offset
-
)
-
-
respond_to do |format|
-
format.html { head 406 }
-
format.api
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html { head 406 }
-
format.api
-
end
-
end
-
-
1
def create
-
members = []
-
if params[:membership]
-
if params[:membership][:user_ids]
-
attrs = params[:membership].dup
-
user_ids = attrs.delete(:user_ids)
-
user_ids.each do |user_id|
-
members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => user_id)
-
end
-
else
-
members << Member.new(:role_ids => params[:membership][:role_ids], :user_id => params[:membership][:user_id])
-
end
-
@project.members << members
-
end
-
-
respond_to do |format|
-
if members.present? && members.all? {|m| m.valid? }
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
-
format.js {
-
render(:update) {|page|
-
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
-
page << 'hideOnLoad()'
-
members.each {|member| page.visual_effect(:highlight, "member-#{member.id}") }
-
}
-
}
-
format.api {
-
@member = members.first
-
render :action => 'show', :status => :created, :location => membership_url(@member)
-
}
-
else
-
format.js {
-
render(:update) {|page|
-
errors = members.collect {|m|
-
m.errors.full_messages
-
}.flatten.uniq
-
-
page.alert(l(:notice_failed_to_save_members, :errors => errors.join(', ')))
-
}
-
}
-
format.api { render_validation_errors(members.first) }
-
end
-
end
-
end
-
-
1
def update
-
if params[:membership]
-
@member.role_ids = params[:membership][:role_ids]
-
end
-
saved = @member.save
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
-
format.js {
-
render(:update) {|page|
-
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
-
page << 'hideOnLoad()'
-
page.visual_effect(:highlight, "member-#{@member.id}")
-
}
-
}
-
format.api {
-
if saved
-
head :ok
-
else
-
render_validation_errors(@member)
-
end
-
}
-
end
-
end
-
-
1
def destroy
-
if request.delete? && @member.deletable?
-
@member.destroy
-
end
-
respond_to do |format|
-
format.html { redirect_to :controller => 'projects', :action => 'settings', :tab => 'members', :id => @project }
-
format.js { render(:update) {|page|
-
page.replace_html "tab-content-members", :partial => 'projects/settings/members'
-
page << 'hideOnLoad()'
-
}
-
}
-
format.api {
-
if @member.destroyed?
-
head :ok
-
else
-
head :unprocessable_entity
-
end
-
}
-
end
-
end
-
-
1
def autocomplete
-
@principals = Principal.active.not_member_of(@project).like(params[:q]).all(:limit => 100)
-
render :layout => false
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MessagesController < ApplicationController
-
1
menu_item :boards
-
1
default_search_scope :messages
-
1
before_filter :find_board, :only => [:new, :preview]
-
1
before_filter :find_message, :except => [:new, :preview]
-
1
before_filter :authorize, :except => [:preview, :edit, :destroy]
-
-
1
helper :watchers
-
1
helper :attachments
-
1
include AttachmentsHelper
-
-
1
REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
-
-
# Show a topic and its replies
-
1
def show
-
page = params[:page]
-
# Find the page of the requested reply
-
if params[:r] && page.nil?
-
offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
-
page = 1 + offset / REPLIES_PER_PAGE
-
end
-
-
@reply_count = @topic.children.count
-
@reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
-
@replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
-
:order => "#{Message.table_name}.created_on ASC",
-
:limit => @reply_pages.items_per_page,
-
:offset => @reply_pages.current.offset)
-
-
@reply = Message.new(:subject => "RE: #{@message.subject}")
-
render :action => "show", :layout => false if request.xhr?
-
end
-
-
# Create a new topic
-
1
def new
-
@message = Message.new
-
@message.author = User.current
-
@message.board = @board
-
@message.safe_attributes = params[:message]
-
if request.post?
-
@message.save_attachments(params[:attachments])
-
if @message.save
-
call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
-
render_attachment_warning_if_needed(@message)
-
redirect_to :action => 'show', :id => @message
-
end
-
end
-
end
-
-
# Reply to a topic
-
1
def reply
-
@reply = Message.new
-
@reply.author = User.current
-
@reply.board = @board
-
@reply.safe_attributes = params[:reply]
-
@topic.children << @reply
-
if !@reply.new_record?
-
call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
-
attachments = Attachment.attach_files(@reply, params[:attachments])
-
render_attachment_warning_if_needed(@reply)
-
end
-
redirect_to :action => 'show', :id => @topic, :r => @reply
-
end
-
-
# Edit a message
-
1
def edit
-
(render_403; return false) unless @message.editable_by?(User.current)
-
@message.safe_attributes = params[:message]
-
if request.post? && @message.save
-
attachments = Attachment.attach_files(@message, params[:attachments])
-
render_attachment_warning_if_needed(@message)
-
flash[:notice] = l(:notice_successful_update)
-
@message.reload
-
redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
-
end
-
end
-
-
# Delete a messages
-
1
def destroy
-
(render_403; return false) unless @message.destroyable_by?(User.current)
-
r = @message.to_param
-
@message.destroy
-
redirect_to @message.parent.nil? ?
-
{ :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
-
{ :action => 'show', :id => @message.parent, :r => r }
-
end
-
-
1
def quote
-
user = @message.author
-
text = @message.content
-
subject = @message.subject.gsub('"', '\"')
-
subject = "RE: #{subject}" unless subject.starts_with?('RE:')
-
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
-
content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
-
render(:update) { |page|
-
page << "$('message_subject').value = \"#{subject}\";"
-
page.<< "$('message_content').value = \"#{content}\";"
-
page.show 'reply'
-
page << "Form.Element.focus('message_content');"
-
page << "Element.scrollTo('reply');"
-
page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
-
}
-
end
-
-
1
def preview
-
message = @board.messages.find_by_id(params[:id])
-
@attachements = message.attachments if message
-
@text = (params[:message] || params[:reply])[:content]
-
render :partial => 'common/preview'
-
end
-
-
1
private
-
1
def find_message
-
find_board
-
@message = @board.messages.find(params[:id], :include => :parent)
-
@topic = @message.root
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_board
-
@board = Board.find(params[:board_id], :include => :project)
-
@project = @board.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MyController < ApplicationController
-
1
before_filter :require_login
-
-
1
helper :issues
-
1
helper :users
-
1
helper :custom_fields
-
-
1
BLOCKS = { 'issuesassignedtome' => :label_assigned_to_me_issues,
-
'issuesreportedbyme' => :label_reported_issues,
-
'issueswatched' => :label_watched_issues,
-
'news' => :label_news_latest,
-
'calendar' => :label_calendar,
-
'documents' => :label_document_plural,
-
'timelog' => :label_spent_time
-
}.merge(Redmine::Views::MyPage::Block.additional_blocks).freeze
-
-
1
DEFAULT_LAYOUT = { 'left' => ['issuesassignedtome'],
-
'right' => ['issuesreportedbyme']
-
}.freeze
-
-
1
def index
-
page
-
render :action => 'page'
-
end
-
-
# Show user's page
-
1
def page
-
123
@user = User.current
-
123
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT
-
end
-
-
# Edit user's account
-
1
def account
-
@user = User.current
-
@pref = @user.pref
-
if request.post?
-
@user.safe_attributes = params[:user]
-
@user.pref.attributes = params[:pref]
-
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
-
if @user.save
-
@user.pref.save
-
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
-
set_language_if_valid @user.language
-
flash[:notice] = l(:notice_account_updated)
-
redirect_to :action => 'account'
-
return
-
end
-
end
-
end
-
-
# Destroys user's account
-
1
def destroy
-
@user = User.current
-
unless @user.own_account_deletable?
-
redirect_to :action => 'account'
-
return
-
end
-
-
if request.post? && params[:confirm]
-
@user.destroy
-
if @user.destroyed?
-
logout_user
-
flash[:notice] = l(:notice_account_deleted)
-
end
-
redirect_to home_path
-
end
-
end
-
-
# Manage user's password
-
1
def password
-
@user = User.current
-
unless @user.change_password_allowed?
-
flash[:error] = l(:notice_can_t_change_password)
-
redirect_to :action => 'account'
-
return
-
end
-
if request.post?
-
if @user.check_password?(params[:password])
-
@user.password, @user.password_confirmation = params[:new_password], params[:new_password_confirmation]
-
if @user.save
-
flash[:notice] = l(:notice_account_password_updated)
-
redirect_to :action => 'account'
-
end
-
else
-
flash[:error] = l(:notice_account_wrong_password)
-
end
-
end
-
end
-
-
# Create a new feeds key
-
1
def reset_rss_key
-
if request.post?
-
if User.current.rss_token
-
User.current.rss_token.destroy
-
User.current.reload
-
end
-
User.current.rss_key
-
flash[:notice] = l(:notice_feeds_access_key_reseted)
-
end
-
redirect_to :action => 'account'
-
end
-
-
# Create a new API key
-
1
def reset_api_key
-
if request.post?
-
if User.current.api_token
-
User.current.api_token.destroy
-
User.current.reload
-
end
-
User.current.api_key
-
flash[:notice] = l(:notice_api_access_key_reseted)
-
end
-
redirect_to :action => 'account'
-
end
-
-
# User's page layout configuration
-
1
def page_layout
-
@user = User.current
-
@blocks = @user.pref[:my_page_layout] || DEFAULT_LAYOUT.dup
-
@block_options = []
-
BLOCKS.each {|k, v| @block_options << [l("my.blocks.#{v}", :default => [v, v.to_s.humanize]), k.dasherize]}
-
end
-
-
# Add a block to user's page
-
# The block is added on top of the page
-
# params[:block] : id of the block to add
-
1
def add_block
-
block = params[:block].to_s.underscore
-
(render :nothing => true; return) unless block && (BLOCKS.keys.include? block)
-
@user = User.current
-
layout = @user.pref[:my_page_layout] || {}
-
# remove if already present in a group
-
%w(top left right).each {|f| (layout[f] ||= []).delete block }
-
# add it on top
-
layout['top'].unshift block
-
@user.pref[:my_page_layout] = layout
-
@user.pref.save
-
render :partial => "block", :locals => {:user => @user, :block_name => block}
-
end
-
-
# Remove a block to user's page
-
# params[:block] : id of the block to remove
-
1
def remove_block
-
block = params[:block].to_s.underscore
-
@user = User.current
-
# remove block in all groups
-
layout = @user.pref[:my_page_layout] || {}
-
%w(top left right).each {|f| (layout[f] ||= []).delete block }
-
@user.pref[:my_page_layout] = layout
-
@user.pref.save
-
render :nothing => true
-
end
-
-
# Change blocks order on user's page
-
# params[:group] : group to order (top, left or right)
-
# params[:list-(top|left|right)] : array of block ids of the group
-
1
def order_blocks
-
group = params[:group]
-
@user = User.current
-
if group.is_a?(String)
-
group_items = (params["list-#{group}"] || []).collect(&:underscore)
-
if group_items and group_items.is_a? Array
-
layout = @user.pref[:my_page_layout] || {}
-
# remove group blocks if they are presents in other groups
-
%w(top left right).each {|f|
-
layout[f] = (layout[f] || []) - group_items
-
}
-
layout[group] = group_items
-
@user.pref[:my_page_layout] = layout
-
@user.pref.save
-
end
-
end
-
render :nothing => true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class NewsController < ApplicationController
-
1
default_search_scope :news
-
1
model_object News
-
1
before_filter :find_model_object, :except => [:new, :create, :index]
-
1
before_filter :find_project_from_association, :except => [:new, :create, :index]
-
1
before_filter :find_project_by_project_id, :only => [:new, :create]
-
1
before_filter :authorize, :except => [:index]
-
1
before_filter :find_optional_project, :only => :index
-
1
accept_rss_auth :index
-
1
accept_api_auth :index
-
-
1
helper :watchers
-
1
helper :attachments
-
-
1
def index
-
case params[:format]
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
@limit = 10
-
end
-
-
scope = @project ? @project.news.visible : News.visible
-
-
@news_count = scope.count
-
@news_pages = Paginator.new self, @news_count, @limit, params['page']
-
@offset ||= @news_pages.current.offset
-
@newss = scope.all(:include => [:author, :project],
-
:order => "#{News.table_name}.created_on DESC",
-
:offset => @offset,
-
:limit => @limit)
-
-
respond_to do |format|
-
format.html {
-
@news = News.new # for adding news inline
-
render :layout => false if request.xhr?
-
}
-
format.api
-
format.atom { render_feed(@newss, :title => (@project ? @project.name : Setting.app_title) + ": #{l(:label_news_plural)}") }
-
end
-
end
-
-
1
def show
-
@comments = @news.comments
-
@comments.reverse! if User.current.wants_comments_in_reverse_order?
-
end
-
-
1
def new
-
@news = News.new(:project => @project, :author => User.current)
-
end
-
-
1
def create
-
@news = News.new(:project => @project, :author => User.current)
-
@news.safe_attributes = params[:news]
-
@news.save_attachments(params[:attachments])
-
if @news.save
-
render_attachment_warning_if_needed(@news)
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'news', :action => 'index', :project_id => @project
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@news.safe_attributes = params[:news]
-
@news.save_attachments(params[:attachments])
-
if @news.save
-
render_attachment_warning_if_needed(@news)
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'show', :id => @news
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@news.destroy
-
redirect_to :action => 'index', :project_id => @project
-
end
-
-
1
private
-
-
1
def find_optional_project
-
return true unless params[:project_id]
-
@project = Project.find(params[:project_id])
-
authorize
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class PreviewsController < ApplicationController
-
1
before_filter :find_project
-
-
1
def issue
-
@issue = @project.issues.find_by_id(params[:id]) unless params[:id].blank?
-
if @issue
-
@attachements = @issue.attachments
-
@description = params[:issue] && params[:issue][:description]
-
if @description && @description.gsub(/(\r?\n|\n\r?)/, "\n") == @issue.description.to_s.gsub(/(\r?\n|\n\r?)/, "\n")
-
@description = nil
-
end
-
@notes = params[:notes]
-
else
-
@description = (params[:issue] ? params[:issue][:description] : nil)
-
end
-
render :layout => false
-
end
-
-
1
def news
-
@text = (params[:news] ? params[:news][:description] : nil)
-
render :partial => 'common/preview'
-
end
-
-
1
private
-
-
1
def find_project
-
project_id = (params[:issue] && params[:issue][:project_id]) || params[:project_id]
-
@project = Project.find(project_id)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ProjectEnumerationsController < ApplicationController
-
1
before_filter :find_project_by_project_id
-
1
before_filter :authorize
-
-
1
def update
-
if request.put? && params[:enumerations]
-
Project.transaction do
-
params[:enumerations].each do |id, activity|
-
@project.update_or_create_time_entry_activity(id, activity)
-
end
-
end
-
flash[:notice] = l(:notice_successful_update)
-
end
-
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
-
end
-
-
1
def destroy
-
@project.time_entry_activities.each do |time_entry_activity|
-
time_entry_activity.destroy(time_entry_activity.parent)
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'activities', :id => @project
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ProjectsController < ApplicationController
-
1
menu_item :overview
-
1
menu_item :roadmap, :only => :roadmap
-
1
menu_item :settings, :only => :settings
-
-
1
before_filter :find_project, :except => [ :index, :list, :new, :create, :copy ]
-
1
before_filter :authorize, :except => [ :index, :list, :new, :create, :copy, :archive, :unarchive, :destroy]
-
1
before_filter :authorize_global, :only => [:new, :create]
-
1
before_filter :require_admin, :only => [ :copy, :archive, :unarchive, :destroy ]
-
1
accept_rss_auth :index
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
after_filter :only => [:create, :edit, :update, :archive, :unarchive, :destroy] do |controller|
-
if controller.request.post?
-
controller.send :expire_action, :controller => 'welcome', :action => 'robots'
-
end
-
end
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
1
helper :issues
-
1
helper :queries
-
1
include QueriesHelper
-
1
helper :repositories
-
1
include RepositoriesHelper
-
1
include ProjectsHelper
-
-
# Lists visible projects
-
1
def index
-
respond_to do |format|
-
format.html {
-
@projects = Project.visible.find(:all, :order => 'lft')
-
}
-
format.api {
-
@offset, @limit = api_offset_and_limit
-
@project_count = Project.visible.count
-
@projects = Project.visible.all(:offset => @offset, :limit => @limit, :order => 'lft')
-
}
-
format.atom {
-
projects = Project.visible.find(:all, :order => 'created_on DESC',
-
:limit => Setting.feeds_limit.to_i)
-
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
-
}
-
end
-
end
-
-
1
def new
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@trackers = Tracker.all
-
@project = Project.new
-
@project.safe_attributes = params[:project]
-
end
-
-
1
def create
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@trackers = Tracker.all
-
@project = Project.new
-
@project.safe_attributes = params[:project]
-
-
if validate_parent_id && @project.save
-
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
-
# Add current user as a project member if he is not admin
-
unless User.current.admin?
-
r = Role.givable.find_by_id(Setting.new_project_user_role_id.to_i) || Role.givable.first
-
m = Member.new(:user => User.current, :roles => [r])
-
@project.members << m
-
end
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to(params[:continue] ?
-
{:controller => 'projects', :action => 'new', :project => {:parent_id => @project.parent_id}.reject {|k,v| v.nil?}} :
-
{:controller => 'projects', :action => 'settings', :id => @project}
-
)
-
}
-
format.api { render :action => 'show', :status => :created, :location => url_for(:controller => 'projects', :action => 'show', :id => @project.id) }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@project) }
-
end
-
end
-
-
end
-
-
1
def copy
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@trackers = Tracker.all
-
@root_projects = Project.find(:all,
-
:conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}",
-
:order => 'name')
-
@source_project = Project.find(params[:id])
-
if request.get?
-
@project = Project.copy_from(@source_project)
-
if @project
-
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
-
else
-
redirect_to :controller => 'admin', :action => 'projects'
-
end
-
else
-
Mailer.with_deliveries(params[:notifications] == '1') do
-
@project = Project.new
-
@project.safe_attributes = params[:project]
-
if validate_parent_id && @project.copy(@source_project, :only => params[:only])
-
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project
-
elsif !@project.new_record?
-
# Project was created
-
# But some objects were not copied due to validation failures
-
# (eg. issues from disabled trackers)
-
# TODO: inform about that
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project
-
end
-
end
-
end
-
rescue ActiveRecord::RecordNotFound
-
redirect_to :controller => 'admin', :action => 'projects'
-
end
-
-
# Show @project
-
1
def show
-
67
if params[:jump]
-
# try to redirect to the requested menu item
-
redirect_to_project_menu_item(@project, params[:jump]) && return
-
end
-
-
67
@users_by_role = @project.users_by_role
-
67
@subprojects = @project.children.visible.all
-
67
@news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
-
67
@trackers = @project.rolled_up_trackers
-
-
67
cond = @project.project_condition(Setting.display_subprojects_issues?)
-
-
67
@open_issues_by_tracker = Issue.visible.count(:group => :tracker,
-
:include => [:project, :status, :tracker],
-
:conditions => ["(#{cond}) AND #{IssueStatus.table_name}.is_closed=?", false])
-
67
@total_issues_by_tracker = Issue.visible.count(:group => :tracker,
-
:include => [:project, :status, :tracker],
-
:conditions => cond)
-
-
67
if User.current.allowed_to?(:view_time_entries, @project)
-
67
@total_hours = TimeEntry.visible.sum(:hours, :include => :project, :conditions => cond).to_f
-
end
-
-
67
@key = User.current.rss_key
-
-
67
respond_to do |format|
-
67
format.html
-
67
format.api
-
end
-
end
-
-
1
def settings
-
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
-
@issue_category ||= IssueCategory.new
-
@member ||= @project.members.new
-
@trackers = Tracker.all
-
@wiki ||= @project.wiki
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@project.safe_attributes = params[:project]
-
if validate_parent_id && @project.save
-
@project.set_allowed_parent!(params[:project]['parent_id']) if params[:project].has_key?('parent_id')
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'settings', :id => @project
-
}
-
format.api { head :ok }
-
end
-
else
-
respond_to do |format|
-
format.html {
-
settings
-
render :action => 'settings'
-
}
-
format.api { render_validation_errors(@project) }
-
end
-
end
-
end
-
-
1
def modules
-
@project.enabled_module_names = params[:enabled_module_names]
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'settings', :id => @project, :tab => 'modules'
-
end
-
-
1
def archive
-
if request.post?
-
unless @project.archive
-
flash[:error] = l(:error_can_not_archive_project)
-
end
-
end
-
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
-
end
-
-
1
def unarchive
-
@project.unarchive if request.post? && !@project.active?
-
redirect_to(url_for(:controller => 'admin', :action => 'projects', :status => params[:status]))
-
end
-
-
# Delete @project
-
1
def destroy
-
@project_to_destroy = @project
-
if api_request? || params[:confirm]
-
@project_to_destroy.destroy
-
respond_to do |format|
-
format.html { redirect_to :controller => 'admin', :action => 'projects' }
-
format.api { head :ok }
-
end
-
end
-
# hide project in layout
-
@project = nil
-
end
-
-
1
private
-
-
# Validates parent_id param according to user's permissions
-
# TODO: move it to Project model in a validation that depends on User.current
-
1
def validate_parent_id
-
return true if User.current.admin?
-
parent_id = params[:project] && params[:project][:parent_id]
-
if parent_id || @project.new_record?
-
parent = parent_id.blank? ? nil : Project.find_by_id(parent_id.to_i)
-
unless @project.allowed_parents.include?(parent)
-
@project.errors.add :parent_id, :invalid
-
return false
-
end
-
end
-
true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class QueriesController < ApplicationController
-
1
menu_item :issues
-
1
before_filter :find_query, :except => [:new, :create, :index]
-
1
before_filter :find_optional_project, :only => [:new, :create]
-
-
1
accept_api_auth :index
-
-
1
include QueriesHelper
-
-
1
def index
-
case params[:format]
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
@limit = per_page_option
-
end
-
-
@query_count = Query.visible.count
-
@query_pages = Paginator.new self, @query_count, @limit, params['page']
-
@queries = Query.visible.all(:limit => @limit, :offset => @offset, :order => "#{Query.table_name}.name")
-
-
respond_to do |format|
-
format.html { render :nothing => true }
-
format.api
-
end
-
end
-
-
1
def new
-
@query = Query.new
-
@query.user = User.current
-
@query.project = @project
-
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
-
build_query_from_params
-
end
-
-
1
def create
-
@query = Query.new(params[:query])
-
@query.user = User.current
-
@query.project = params[:query_is_for_all] ? nil : @project
-
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
-
build_query_from_params
-
@query.column_names = nil if params[:default_columns]
-
-
if @query.save
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
-
else
-
render :action => 'new', :layout => !request.xhr?
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
@query.attributes = params[:query]
-
@query.project = nil if params[:query_is_for_all]
-
@query.is_public = false unless User.current.allowed_to?(:manage_public_queries, @project) || User.current.admin?
-
build_query_from_params
-
@query.column_names = nil if params[:default_columns]
-
-
if @query.save
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :query_id => @query
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@query.destroy
-
redirect_to :controller => 'issues', :action => 'index', :project_id => @project, :set_filter => 1
-
end
-
-
1
private
-
1
def find_query
-
@query = Query.find(params[:id])
-
@project = @query.project
-
render_403 unless @query.editable_by?(User.current)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_optional_project
-
@project = Project.find(params[:project_id]) if params[:project_id]
-
render_403 unless User.current.allowed_to?(:save_queries, @project, :global => true)
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ReportsController < ApplicationController
-
1
menu_item :issues
-
1
before_filter :find_project, :authorize, :find_issue_statuses
-
-
1
def issue_report
-
@trackers = @project.trackers
-
@versions = @project.shared_versions.sort
-
@priorities = IssuePriority.all
-
@categories = @project.issue_categories
-
@assignees = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
-
@authors = @project.users.sort
-
@subprojects = @project.descendants.visible
-
-
@issues_by_tracker = Issue.by_tracker(@project)
-
@issues_by_version = Issue.by_version(@project)
-
@issues_by_priority = Issue.by_priority(@project)
-
@issues_by_category = Issue.by_category(@project)
-
@issues_by_assigned_to = Issue.by_assigned_to(@project)
-
@issues_by_author = Issue.by_author(@project)
-
@issues_by_subproject = Issue.by_subproject(@project) || []
-
-
render :template => "reports/issue_report"
-
end
-
-
1
def issue_report_details
-
case params[:detail]
-
when "tracker"
-
@field = "tracker_id"
-
@rows = @project.trackers
-
@data = Issue.by_tracker(@project)
-
@report_title = l(:field_tracker)
-
when "version"
-
@field = "fixed_version_id"
-
@rows = @project.shared_versions.sort
-
@data = Issue.by_version(@project)
-
@report_title = l(:field_version)
-
when "priority"
-
@field = "priority_id"
-
@rows = IssuePriority.all
-
@data = Issue.by_priority(@project)
-
@report_title = l(:field_priority)
-
when "category"
-
@field = "category_id"
-
@rows = @project.issue_categories
-
@data = Issue.by_category(@project)
-
@report_title = l(:field_category)
-
when "assigned_to"
-
@field = "assigned_to_id"
-
@rows = (Setting.issue_group_assignment? ? @project.principals : @project.users).sort
-
@data = Issue.by_assigned_to(@project)
-
@report_title = l(:field_assigned_to)
-
when "author"
-
@field = "author_id"
-
@rows = @project.users.sort
-
@data = Issue.by_author(@project)
-
@report_title = l(:field_author)
-
when "subproject"
-
@field = "project_id"
-
@rows = @project.descendants.visible
-
@data = Issue.by_subproject(@project) || []
-
@report_title = l(:field_subproject)
-
end
-
-
respond_to do |format|
-
if @field
-
format.html {}
-
else
-
format.html { redirect_to :action => 'issue_report', :id => @project }
-
end
-
end
-
end
-
-
1
private
-
-
1
def find_issue_statuses
-
@statuses = IssueStatus.find(:all, :order => 'position')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'SVG/Graph/Bar'
-
1
require 'SVG/Graph/BarHorizontal'
-
1
require 'digest/sha1'
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
class ChangesetNotFound < Exception; end
-
1
class InvalidRevisionParam < Exception; end
-
-
1
class RepositoriesController < ApplicationController
-
1
menu_item :repository
-
1
menu_item :settings, :only => [:new, :create, :edit, :update, :destroy, :committers]
-
1
default_search_scope :changesets
-
-
1
before_filter :find_project_by_project_id, :only => [:new, :create]
-
1
before_filter :find_repository, :only => [:edit, :update, :destroy, :committers]
-
1
before_filter :find_project_repository, :except => [:new, :create, :edit, :update, :destroy, :committers]
-
1
before_filter :find_changeset, :only => [:revision, :add_related_issue, :remove_related_issue]
-
1
before_filter :authorize
-
1
accept_rss_auth :revisions
-
-
1
rescue_from Redmine::Scm::Adapters::CommandFailed, :with => :show_error_command_failed
-
-
1
def new
-
scm = params[:repository_scm] || (Redmine::Scm::Base.all & Setting.enabled_scm).first
-
@repository = Repository.factory(scm)
-
@repository.is_default = @project.repository.nil?
-
@repository.project = @project
-
render :layout => !request.xhr?
-
end
-
-
1
def create
-
attrs = pickup_extra_info
-
@repository = Repository.factory(params[:repository_scm], attrs[:attrs])
-
if attrs[:attrs_extra].keys.any?
-
@repository.merge_extra_info(attrs[:attrs_extra])
-
end
-
@repository.project = @project
-
if request.post? && @repository.save
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
else
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
attrs = pickup_extra_info
-
@repository.attributes = attrs[:attrs]
-
if attrs[:attrs_extra].keys.any?
-
@repository.merge_extra_info(attrs[:attrs_extra])
-
end
-
@repository.project = @project
-
if request.put? && @repository.save
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def pickup_extra_info
-
p = {}
-
p_extra = {}
-
params[:repository].each do |k, v|
-
if k =~ /^extra_/
-
p_extra[k] = v
-
else
-
p[k] = v
-
end
-
end
-
{:attrs => p, :attrs_extra => p_extra}
-
end
-
1
private :pickup_extra_info
-
-
1
def committers
-
@committers = @repository.committers
-
@users = @project.users
-
additional_user_ids = @committers.collect(&:last).collect(&:to_i) - @users.collect(&:id)
-
@users += User.find_all_by_id(additional_user_ids) unless additional_user_ids.empty?
-
@users.compact!
-
@users.sort!
-
if request.post? && params[:committers].is_a?(Hash)
-
# Build a hash with repository usernames as keys and corresponding user ids as values
-
@repository.committer_ids = params[:committers].values.inject({}) {|h, c| h[c.first] = c.last; h}
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
end
-
end
-
-
1
def destroy
-
@repository.destroy if request.delete?
-
redirect_to settings_project_path(@project, :tab => 'repositories')
-
end
-
-
1
def show
-
@repository.fetch_changesets if Setting.autofetch_changesets? && @path.empty?
-
-
@entries = @repository.entries(@path, @rev)
-
@changeset = @repository.find_changeset_by_name(@rev)
-
if request.xhr?
-
@entries ? render(:partial => 'dir_list_content') : render(:nothing => true)
-
else
-
(show_error_not_found; return) unless @entries
-
@changesets = @repository.latest_changesets(@path, @rev)
-
@properties = @repository.properties(@path, @rev)
-
@repositories = @project.repositories
-
render :action => 'show'
-
end
-
end
-
-
1
alias_method :browse, :show
-
-
1
def changes
-
@entry = @repository.entry(@path, @rev)
-
(show_error_not_found; return) unless @entry
-
@changesets = @repository.latest_changesets(@path, @rev, Setting.repository_log_display_limit.to_i)
-
@properties = @repository.properties(@path, @rev)
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
-
1
def revisions
-
@changeset_count = @repository.changesets.count
-
@changeset_pages = Paginator.new self, @changeset_count,
-
per_page_option,
-
params['page']
-
@changesets = @repository.changesets.find(:all,
-
:limit => @changeset_pages.items_per_page,
-
:offset => @changeset_pages.current.offset,
-
:include => [:user, :repository, :parents])
-
-
respond_to do |format|
-
format.html { render :layout => false if request.xhr? }
-
format.atom { render_feed(@changesets, :title => "#{@project.name}: #{l(:label_revision_plural)}") }
-
end
-
end
-
-
1
def raw
-
entry_and_raw(true)
-
end
-
-
1
def entry
-
entry_and_raw(false)
-
end
-
-
1
def entry_and_raw(is_raw)
-
@entry = @repository.entry(@path, @rev)
-
(show_error_not_found; return) unless @entry
-
-
# If the entry is a dir, show the browser
-
(show; return) if @entry.is_dir?
-
-
@content = @repository.cat(@path, @rev)
-
(show_error_not_found; return) unless @content
-
if is_raw ||
-
(@content.size && @content.size > Setting.file_max_size_displayed.to_i.kilobyte) ||
-
! is_entry_text_data?(@content, @path)
-
# Force the download
-
send_opt = { :filename => filename_for_content_disposition(@path.split('/').last) }
-
send_type = Redmine::MimeType.of(@path)
-
send_opt[:type] = send_type.to_s if send_type
-
send_data @content, send_opt
-
else
-
# Prevent empty lines when displaying a file with Windows style eol
-
# TODO: UTF-16
-
# Is this needs? AttachmentsController reads file simply.
-
@content.gsub!("\r\n", "\n")
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
end
-
1
private :entry_and_raw
-
-
1
def is_entry_text_data?(ent, path)
-
# UTF-16 contains "\x00".
-
# It is very strict that file contains less than 30% of ascii symbols
-
# in non Western Europe.
-
return true if Redmine::MimeType.is_type?('text', path)
-
# Ruby 1.8.6 has a bug of integer divisions.
-
# http://apidock.com/ruby/v1_8_6_287/String/is_binary_data%3F
-
return false if ent.is_binary_data?
-
true
-
end
-
1
private :is_entry_text_data?
-
-
1
def annotate
-
@entry = @repository.entry(@path, @rev)
-
(show_error_not_found; return) unless @entry
-
-
@annotate = @repository.scm.annotate(@path, @rev)
-
if @annotate.nil? || @annotate.empty?
-
(render_error l(:error_scm_annotate); return)
-
end
-
ann_buf_size = 0
-
@annotate.lines.each do |buf|
-
ann_buf_size += buf.size
-
end
-
if ann_buf_size > Setting.file_max_size_displayed.to_i.kilobyte
-
(render_error l(:error_scm_annotate_big_text_file); return)
-
end
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
-
1
def revision
-
respond_to do |format|
-
format.html
-
format.js {render :layout => false}
-
end
-
end
-
-
# Adds a related issue to a changeset
-
# POST /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues
-
1
def add_related_issue
-
@issue = @changeset.find_referenced_issue_by_id(params[:issue_id])
-
if @issue && (!@issue.visible? || @changeset.issues.include?(@issue))
-
@issue = nil
-
end
-
-
if @issue
-
@changeset.issues << @issue
-
respond_to do |format|
-
format.js {
-
render :update do |page|
-
page.replace_html "related-issues", :partial => "related_issues"
-
page.visual_effect :highlight, "related-issue-#{@issue.id}"
-
end
-
}
-
end
-
else
-
respond_to do |format|
-
format.js {
-
render :update do |page|
-
page.alert(l(:label_issue) + ' ' + l('activerecord.errors.messages.invalid'))
-
end
-
}
-
end
-
end
-
end
-
-
# Removes a related issue from a changeset
-
# DELETE /projects/:project_id/repository/(:repository_id/)revisions/:rev/issues/:issue_id
-
1
def remove_related_issue
-
@issue = Issue.visible.find_by_id(params[:issue_id])
-
if @issue
-
@changeset.issues.delete(@issue)
-
end
-
-
respond_to do |format|
-
format.js {
-
render :update do |page|
-
page.remove "related-issue-#{@issue.id}"
-
end if @issue
-
}
-
end
-
end
-
-
1
def diff
-
if params[:format] == 'diff'
-
@diff = @repository.diff(@path, @rev, @rev_to)
-
(show_error_not_found; return) unless @diff
-
filename = "changeset_r#{@rev}"
-
filename << "_r#{@rev_to}" if @rev_to
-
send_data @diff.join, :filename => "#{filename}.diff",
-
:type => 'text/x-patch',
-
:disposition => 'attachment'
-
else
-
@diff_type = params[:type] || User.current.pref[:diff_type] || 'inline'
-
@diff_type = 'inline' unless %w(inline sbs).include?(@diff_type)
-
-
# Save diff type as user preference
-
if User.current.logged? && @diff_type != User.current.pref[:diff_type]
-
User.current.pref[:diff_type] = @diff_type
-
User.current.preference.save
-
end
-
@cache_key = "repositories/diff/#{@repository.id}/" +
-
Digest::MD5.hexdigest("#{@path}-#{@rev}-#{@rev_to}-#{@diff_type}-#{current_language}")
-
unless read_fragment(@cache_key)
-
@diff = @repository.diff(@path, @rev, @rev_to)
-
show_error_not_found unless @diff
-
end
-
-
@changeset = @repository.find_changeset_by_name(@rev)
-
@changeset_to = @rev_to ? @repository.find_changeset_by_name(@rev_to) : nil
-
@diff_format_revisions = @repository.diff_format_revisions(@changeset, @changeset_to)
-
end
-
end
-
-
1
def stats
-
end
-
-
1
def graph
-
data = nil
-
case params[:graph]
-
when "commits_per_month"
-
data = graph_commits_per_month(@repository)
-
when "commits_per_author"
-
data = graph_commits_per_author(@repository)
-
end
-
if data
-
headers["Content-Type"] = "image/svg+xml"
-
send_data(data, :type => "image/svg+xml", :disposition => "inline")
-
else
-
render_404
-
end
-
end
-
-
1
private
-
-
1
def find_repository
-
@repository = Repository.find(params[:id])
-
@project = @repository.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
REV_PARAM_RE = %r{\A[a-f0-9]*\Z}i
-
-
1
def find_project_repository
-
@project = Project.find(params[:id])
-
if params[:repository_id].present?
-
@repository = @project.repositories.find_by_identifier_param(params[:repository_id])
-
else
-
@repository = @project.repository
-
end
-
(render_404; return false) unless @repository
-
@path = params[:path].is_a?(Array) ? params[:path].join('/') : params[:path].to_s
-
@rev = params[:rev].blank? ? @repository.default_branch : params[:rev].to_s.strip
-
@rev_to = params[:rev_to]
-
-
unless @rev.to_s.match(REV_PARAM_RE) && @rev_to.to_s.match(REV_PARAM_RE)
-
if @repository.branches.blank?
-
raise InvalidRevisionParam
-
end
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
rescue InvalidRevisionParam
-
show_error_not_found
-
end
-
-
1
def find_changeset
-
if @rev.present?
-
@changeset = @repository.find_changeset_by_name(@rev)
-
end
-
show_error_not_found unless @changeset
-
end
-
-
1
def show_error_not_found
-
render_error :message => l(:error_scm_not_found), :status => 404
-
end
-
-
# Handler for Redmine::Scm::Adapters::CommandFailed exception
-
1
def show_error_command_failed(exception)
-
render_error l(:error_scm_command_failed, exception.message)
-
end
-
-
1
def graph_commits_per_month(repository)
-
@date_to = Date.today
-
@date_from = @date_to << 11
-
@date_from = Date.civil(@date_from.year, @date_from.month, 1)
-
commits_by_day = Changeset.count(
-
:all, :group => :commit_date,
-
:conditions => ["repository_id = ? AND commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
-
commits_by_month = [0] * 12
-
commits_by_day.each {|c| commits_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
-
-
changes_by_day = Change.count(
-
:all, :group => :commit_date, :include => :changeset,
-
:conditions => ["#{Changeset.table_name}.repository_id = ? AND #{Changeset.table_name}.commit_date BETWEEN ? AND ?", repository.id, @date_from, @date_to])
-
changes_by_month = [0] * 12
-
changes_by_day.each {|c| changes_by_month[(@date_to.month - c.first.to_date.month) % 12] += c.last }
-
-
fields = []
-
12.times {|m| fields << month_name(((Date.today.month - 1 - m) % 12) + 1)}
-
-
graph = SVG::Graph::Bar.new(
-
:height => 300,
-
:width => 800,
-
:fields => fields.reverse,
-
:stack => :side,
-
:scale_integers => true,
-
:step_x_labels => 2,
-
:show_data_values => false,
-
:graph_title => l(:label_commits_per_month),
-
:show_graph_title => true
-
)
-
-
graph.add_data(
-
:data => commits_by_month[0..11].reverse,
-
:title => l(:label_revision_plural)
-
)
-
-
graph.add_data(
-
:data => changes_by_month[0..11].reverse,
-
:title => l(:label_change_plural)
-
)
-
-
graph.burn
-
end
-
-
1
def graph_commits_per_author(repository)
-
commits_by_author = Changeset.count(:all, :group => :committer, :conditions => ["repository_id = ?", repository.id])
-
commits_by_author.to_a.sort! {|x, y| x.last <=> y.last}
-
-
changes_by_author = Change.count(:all, :group => :committer, :include => :changeset, :conditions => ["#{Changeset.table_name}.repository_id = ?", repository.id])
-
h = changes_by_author.inject({}) {|o, i| o[i.first] = i.last; o}
-
-
fields = commits_by_author.collect {|r| r.first}
-
commits_data = commits_by_author.collect {|r| r.last}
-
changes_data = commits_by_author.collect {|r| h[r.first] || 0}
-
-
fields = fields + [""]*(10 - fields.length) if fields.length<10
-
commits_data = commits_data + [0]*(10 - commits_data.length) if commits_data.length<10
-
changes_data = changes_data + [0]*(10 - changes_data.length) if changes_data.length<10
-
-
# Remove email adress in usernames
-
fields = fields.collect {|c| c.gsub(%r{<.+@.+>}, '') }
-
-
graph = SVG::Graph::BarHorizontal.new(
-
:height => 400,
-
:width => 800,
-
:fields => fields,
-
:stack => :side,
-
:scale_integers => true,
-
:show_data_values => false,
-
:rotate_y_labels => false,
-
:graph_title => l(:label_commits_per_author),
-
:show_graph_title => true
-
)
-
graph.add_data(
-
:data => commits_data,
-
:title => l(:label_revision_plural)
-
)
-
graph.add_data(
-
:data => changes_data,
-
:title => l(:label_change_plural)
-
)
-
graph.burn
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class RolesController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :index
-
1
before_filter :require_admin_or_api_request, :only => :index
-
1
before_filter :find_role, :only => [:edit, :update, :destroy]
-
1
accept_api_auth :index
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@role_pages, @roles = paginate :roles, :per_page => 25, :order => 'builtin, position'
-
render :action => "index", :layout => false if request.xhr?
-
}
-
format.api {
-
@roles = Role.givable.all
-
}
-
end
-
end
-
-
1
def new
-
# Prefills the form with 'Non member' role permissions
-
@role = Role.new(params[:role] || {:permissions => Role.non_member.permissions})
-
@roles = Role.sorted.all
-
end
-
-
1
def create
-
@role = Role.new(params[:role])
-
if request.post? && @role.save
-
# workflow copy
-
if !params[:copy_workflow_from].blank? && (copy_from = Role.find_by_id(params[:copy_workflow_from]))
-
@role.workflows.copy(copy_from)
-
end
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
else
-
@roles = Role.sorted.all
-
render :action => 'new'
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? and @role.update_attributes(params[:role])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
else
-
render :action => 'edit'
-
end
-
end
-
-
1
def destroy
-
@role.destroy
-
redirect_to :action => 'index'
-
rescue
-
flash[:error] = l(:error_can_not_remove_role)
-
redirect_to :action => 'index'
-
end
-
-
1
def permissions
-
@roles = Role.sorted.all
-
@permissions = Redmine::AccessControl.permissions.select { |p| !p.public? }
-
if request.post?
-
@roles.each do |role|
-
role.permissions = params[:permissions][role.id.to_s]
-
role.save
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
end
-
end
-
-
1
private
-
-
1
def find_role
-
@role = Role.find(params[:id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class SearchController < ApplicationController
-
1
before_filter :find_optional_project
-
-
1
helper :messages
-
1
include MessagesHelper
-
-
1
def index
-
@question = params[:q] || ""
-
@question.strip!
-
@all_words = params[:all_words] ? params[:all_words].present? : true
-
@titles_only = params[:titles_only] ? params[:titles_only].present? : false
-
-
projects_to_search =
-
case params[:scope]
-
when 'all'
-
nil
-
when 'my_projects'
-
User.current.memberships.collect(&:project)
-
when 'subprojects'
-
@project ? (@project.self_and_descendants.active.all) : nil
-
else
-
@project
-
end
-
-
offset = nil
-
begin; offset = params[:offset].to_time if params[:offset]; rescue; end
-
-
# quick jump to an issue
-
if @question.match(/^#?(\d+)$/) && Issue.visible.find_by_id($1.to_i)
-
redirect_to :controller => "issues", :action => "show", :id => $1
-
return
-
end
-
-
@object_types = Redmine::Search.available_search_types.dup
-
if projects_to_search.is_a? Project
-
# don't search projects
-
@object_types.delete('projects')
-
# only show what the user is allowed to view
-
@object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
-
end
-
-
@scope = @object_types.select {|t| params[t]}
-
@scope = @object_types if @scope.empty?
-
-
# extract tokens from the question
-
# eg. hello "bye bye" => ["hello", "bye bye"]
-
@tokens = @question.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
-
# tokens must be at least 2 characters long
-
@tokens = @tokens.uniq.select {|w| w.length > 1 }
-
-
if !@tokens.empty?
-
# no more than 5 tokens to search for
-
@tokens.slice! 5..-1 if @tokens.size > 5
-
-
@results = []
-
@results_by_type = Hash.new {|h,k| h[k] = 0}
-
-
limit = 10
-
@scope.each do |s|
-
r, c = s.singularize.camelcase.constantize.search(@tokens, projects_to_search,
-
:all_words => @all_words,
-
:titles_only => @titles_only,
-
:limit => (limit+1),
-
:offset => offset,
-
:before => params[:previous].nil?)
-
@results += r
-
@results_by_type[s] += c
-
end
-
@results = @results.sort {|a,b| b.event_datetime <=> a.event_datetime}
-
if params[:previous].nil?
-
@pagination_previous_date = @results[0].event_datetime if offset && @results[0]
-
if @results.size > limit
-
@pagination_next_date = @results[limit-1].event_datetime
-
@results = @results[0, limit]
-
end
-
else
-
@pagination_next_date = @results[-1].event_datetime if offset && @results[-1]
-
if @results.size > limit
-
@pagination_previous_date = @results[-(limit)].event_datetime
-
@results = @results[-(limit), limit]
-
end
-
end
-
else
-
@question = ""
-
end
-
render :layout => false if request.xhr?
-
end
-
-
1
private
-
1
def find_optional_project
-
return true unless params[:id]
-
@project = Project.find(params[:id])
-
check_project_privacy
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class SettingsController < ApplicationController
-
1
layout 'admin'
-
1
menu_item :plugins, :only => :plugin
-
-
1
before_filter :require_admin
-
-
1
def index
-
edit
-
render :action => 'edit'
-
end
-
-
1
def edit
-
@notifiables = Redmine::Notifiable.all
-
if request.post? && params[:settings] && params[:settings].is_a?(Hash)
-
settings = (params[:settings] || {}).dup.symbolize_keys
-
settings.each do |name, value|
-
# remove blank values in array settings
-
value.delete_if {|v| v.blank? } if value.is_a?(Array)
-
Setting[name] = value
-
end
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'edit', :tab => params[:tab]
-
else
-
@options = {}
-
@options[:user_format] = User::USER_FORMATS.keys.collect {|f| [User.current.name(f), f.to_s] }
-
@deliveries = ActionMailer::Base.perform_deliveries
-
-
@guessed_host_and_path = request.host_with_port.dup
-
@guessed_host_and_path << ('/'+ Redmine::Utils.relative_url_root.gsub(%r{^\/}, '')) unless Redmine::Utils.relative_url_root.blank?
-
-
Redmine::Themes.rescan
-
end
-
end
-
-
1
def plugin
-
@plugin = Redmine::Plugin.find(params[:id])
-
if request.post?
-
Setting.send "plugin_#{@plugin.id}=", params[:settings]
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'plugin', :id => @plugin.id
-
else
-
@partial = @plugin.settings[:partial]
-
@settings = Setting.send "plugin_#{@plugin.id}"
-
end
-
rescue Redmine::PluginNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class SysController < ActionController::Base
-
1
before_filter :check_enabled
-
-
1
def projects
-
p = Project.active.has_module(:repository).find(
-
:all,
-
:include => :repository,
-
:order => "#{Project.table_name}.identifier"
-
)
-
# extra_info attribute from repository breaks activeresource client
-
render :xml => p.to_xml(
-
:only => [:id, :identifier, :name, :is_public, :status],
-
:include => {:repository => {:only => [:id, :url]}}
-
)
-
end
-
-
1
def create_project_repository
-
project = Project.find(params[:id])
-
if project.repository
-
render :nothing => true, :status => 409
-
else
-
logger.info "Repository for #{project.name} was reported to be created by #{request.remote_ip}."
-
repository = Repository.factory(params[:vendor], params[:repository])
-
repository.project = project
-
if repository.save
-
render :xml => {repository.class.name.underscore.gsub('/', '-') => {:id => repository.id, :url => repository.url}}, :status => 201
-
else
-
render :nothing => true, :status => 422
-
end
-
end
-
end
-
-
1
def fetch_changesets
-
projects = []
-
scope = Project.active.has_module(:repository)
-
if params[:id]
-
project = nil
-
if params[:id].to_s =~ /^\d*$/
-
project = scope.find(params[:id])
-
else
-
project = scope.find_by_identifier(params[:id])
-
end
-
raise ActiveRecord::RecordNotFound unless project
-
projects << project
-
else
-
projects = scope.all
-
end
-
projects.each do |project|
-
project.repositories.each do |repository|
-
repository.fetch_changesets
-
end
-
end
-
render :nothing => true, :status => 200
-
rescue ActiveRecord::RecordNotFound
-
render :nothing => true, :status => 404
-
end
-
-
1
protected
-
-
1
def check_enabled
-
User.current = nil
-
unless Setting.sys_api_enabled? && params[:key].to_s == Setting.sys_api_key
-
render :text => 'Access denied. Repository management WS is disabled or key is invalid.', :status => 403
-
return false
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimelogController < ApplicationController
-
1
menu_item :issues
-
-
1
before_filter :find_project_for_new_time_entry, :only => [:create]
-
1
before_filter :find_time_entry, :only => [:show, :edit, :update]
-
1
before_filter :find_time_entries, :only => [:bulk_edit, :bulk_update, :destroy]
-
1
before_filter :authorize, :except => [:new, :index, :report]
-
-
1
before_filter :find_optional_project, :only => [:index, :report]
-
1
before_filter :find_optional_project_for_new_time_entry, :only => [:new]
-
1
before_filter :authorize_global, :only => [:new, :index, :report]
-
-
1
accept_rss_auth :index
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :issues
-
1
include TimelogHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
-
1
def index
-
4
sort_init 'spent_on', 'desc'
-
sort_update 'spent_on' => 'spent_on',
-
'user' => 'user_id',
-
'activity' => 'activity_id',
-
'project' => "#{Project.table_name}.name",
-
'issue' => 'issue_id',
-
4
'hours' => 'hours'
-
-
4
retrieve_date_range
-
-
4
scope = TimeEntry.visible.spent_between(@from, @to)
-
4
if @issue
-
2
scope = scope.on_issue(@issue)
-
elsif @project
-
2
scope = scope.on_project(@project, Setting.display_subprojects_issues?)
-
end
-
-
4
respond_to do |format|
-
4
format.html {
-
# Paginate results
-
4
@entry_count = scope.count
-
4
@entry_pages = Paginator.new self, @entry_count, per_page_option, params['page']
-
4
@entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => :tracker}],
-
:order => sort_clause,
-
:limit => @entry_pages.items_per_page,
-
:offset => @entry_pages.current.offset
-
)
-
4
@total_hours = scope.sum(:hours).to_f
-
-
4
render :layout => !request.xhr?
-
}
-
4
format.api {
-
@entry_count = scope.count
-
@offset, @limit = api_offset_and_limit
-
@entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => :tracker}],
-
:order => sort_clause,
-
:limit => @limit,
-
:offset => @offset
-
)
-
}
-
4
format.atom {
-
entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => :tracker}],
-
:order => "#{TimeEntry.table_name}.created_on DESC",
-
:limit => Setting.feeds_limit.to_i
-
)
-
render_feed(entries, :title => l(:label_spent_time))
-
}
-
4
format.csv {
-
# Export all entries
-
@entries = scope.all(
-
:include => [:project, :activity, :user, {:issue => [:tracker, :assigned_to, :priority]}],
-
:order => sort_clause
-
)
-
send_data(entries_to_csv(@entries), :type => 'text/csv; header=present', :filename => 'timelog.csv')
-
}
-
end
-
end
-
-
1
def report
-
retrieve_date_range
-
@report = Redmine::Helpers::TimeReport.new(@project, @issue, params[:criteria], params[:columns], @from, @to)
-
-
respond_to do |format|
-
format.html { render :layout => !request.xhr? }
-
format.csv { send_data(report_to_csv(@report), :type => 'text/csv; header=present', :filename => 'timelog.csv') }
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
# TODO: Implement html response
-
format.html { render :nothing => true, :status => 406 }
-
format.api
-
end
-
end
-
-
1
def new
-
2
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
-
2
@time_entry.safe_attributes = params[:time_entry]
-
end
-
-
1
def create
-
2
@time_entry ||= TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => User.current.today)
-
2
@time_entry.safe_attributes = params[:time_entry]
-
-
2
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
-
-
2
if @time_entry.save
-
2
respond_to do |format|
-
2
format.html {
-
2
flash[:notice] = l(:notice_successful_create)
-
2
if params[:continue]
-
if params[:project_id]
-
redirect_to :action => 'new', :project_id => @time_entry.project, :issue_id => @time_entry.issue,
-
:time_entry => {:issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
-
:back_url => params[:back_url]
-
else
-
redirect_to :action => 'new',
-
:time_entry => {:project_id => @time_entry.project_id, :issue_id => @time_entry.issue_id, :activity_id => @time_entry.activity_id},
-
:back_url => params[:back_url]
-
end
-
else
-
2
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
-
end
-
}
-
2
format.api { render :action => 'show', :status => :created, :location => time_entry_url(@time_entry) }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@time_entry) }
-
end
-
end
-
end
-
-
1
def edit
-
@time_entry.safe_attributes = params[:time_entry]
-
end
-
-
1
def update
-
@time_entry.safe_attributes = params[:time_entry]
-
-
call_hook(:controller_timelog_edit_before_save, { :params => params, :time_entry => @time_entry })
-
-
if @time_entry.save
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_back_or_default :action => 'index', :project_id => @time_entry.project
-
}
-
format.api { head :ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@time_entry) }
-
end
-
end
-
end
-
-
1
def bulk_edit
-
@available_activities = TimeEntryActivity.shared.active
-
@custom_fields = TimeEntry.first.available_custom_fields
-
end
-
-
1
def bulk_update
-
attributes = parse_params_for_bulk_time_entry_attributes(params)
-
-
unsaved_time_entry_ids = []
-
@time_entries.each do |time_entry|
-
time_entry.reload
-
time_entry.safe_attributes = attributes
-
call_hook(:controller_time_entries_bulk_edit_before_save, { :params => params, :time_entry => time_entry })
-
unless time_entry.save
-
# Keep unsaved time_entry ids to display them in flash error
-
unsaved_time_entry_ids << time_entry.id
-
end
-
end
-
set_flash_from_bulk_time_entry_save(@time_entries, unsaved_time_entry_ids)
-
redirect_back_or_default({:controller => 'timelog', :action => 'index', :project_id => @projects.first})
-
end
-
-
1
def destroy
-
destroyed = TimeEntry.transaction do
-
@time_entries.each do |t|
-
unless t.destroy && t.destroyed?
-
raise ActiveRecord::Rollback
-
end
-
end
-
end
-
-
respond_to do |format|
-
format.html {
-
if destroyed
-
flash[:notice] = l(:notice_successful_delete)
-
else
-
flash[:error] = l(:notice_unable_delete_time_entry)
-
end
-
redirect_back_or_default(:action => 'index', :project_id => @projects.first)
-
}
-
format.api {
-
if destroyed
-
head :ok
-
else
-
render_validation_errors(@time_entries)
-
end
-
}
-
end
-
end
-
-
1
private
-
1
def find_time_entry
-
@time_entry = TimeEntry.find(params[:id])
-
unless @time_entry.editable_by?(User.current)
-
render_403
-
return false
-
end
-
@project = @time_entry.project
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_time_entries
-
@time_entries = TimeEntry.find_all_by_id(params[:id] || params[:ids])
-
raise ActiveRecord::RecordNotFound if @time_entries.empty?
-
@projects = @time_entries.collect(&:project).compact.uniq
-
@project = @projects.first if @projects.size == 1
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def set_flash_from_bulk_time_entry_save(time_entries, unsaved_time_entry_ids)
-
if unsaved_time_entry_ids.empty?
-
flash[:notice] = l(:notice_successful_update) unless time_entries.empty?
-
else
-
flash[:error] = l(:notice_failed_to_save_time_entries,
-
:count => unsaved_time_entry_ids.size,
-
:total => time_entries.size,
-
:ids => '#' + unsaved_time_entry_ids.join(', #'))
-
end
-
end
-
-
1
def find_optional_project_for_new_time_entry
-
4
if (project_id = (params[:project_id] || params[:time_entry] && params[:time_entry][:project_id])).present?
-
4
@project = Project.find(project_id)
-
end
-
4
if (issue_id = (params[:issue_id] || params[:time_entry] && params[:time_entry][:issue_id])).present?
-
2
@issue = Issue.find(issue_id)
-
2
@project ||= @issue.project
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def find_project_for_new_time_entry
-
2
find_optional_project_for_new_time_entry
-
2
if @project.nil?
-
render_404
-
end
-
end
-
-
1
def find_optional_project
-
4
if !params[:issue_id].blank?
-
2
@issue = Issue.find(params[:issue_id])
-
2
@project = @issue.project
-
2
elsif !params[:project_id].blank?
-
2
@project = Project.find(params[:project_id])
-
end
-
end
-
-
# Retrieves the date range based on predefined ranges or specific from/to param dates
-
1
def retrieve_date_range
-
4
@free_period = false
-
4
@from, @to = nil, nil
-
-
4
if params[:period_type] == '1' || (params[:period_type].nil? && !params[:period].nil?)
-
case params[:period].to_s
-
when 'today'
-
@from = @to = Date.today
-
when 'yesterday'
-
@from = @to = Date.today - 1
-
when 'current_week'
-
@from = Date.today - (Date.today.cwday - 1)%7
-
@to = @from + 6
-
when 'last_week'
-
@from = Date.today - 7 - (Date.today.cwday - 1)%7
-
@to = @from + 6
-
when '7_days'
-
@from = Date.today - 7
-
@to = Date.today
-
when 'current_month'
-
@from = Date.civil(Date.today.year, Date.today.month, 1)
-
@to = (@from >> 1) - 1
-
when 'last_month'
-
@from = Date.civil(Date.today.year, Date.today.month, 1) << 1
-
@to = (@from >> 1) - 1
-
when '30_days'
-
@from = Date.today - 30
-
@to = Date.today
-
when 'current_year'
-
@from = Date.civil(Date.today.year, 1, 1)
-
@to = Date.civil(Date.today.year, 12, 31)
-
end
-
elsif params[:period_type] == '2' || (params[:period_type].nil? && (!params[:from].nil? || !params[:to].nil?))
-
begin; @from = params[:from].to_s.to_date unless params[:from].blank?; rescue; end
-
begin; @to = params[:to].to_s.to_date unless params[:to].blank?; rescue; end
-
@free_period = true
-
else
-
# default
-
end
-
-
4
@from, @to = @to, @from if @from && @to && @from > @to
-
end
-
-
1
def parse_params_for_bulk_time_entry_attributes(params)
-
attributes = (params[:time_entry] || {}).reject {|k,v| v.blank?}
-
attributes.keys.each {|k| attributes[k] = '' if attributes[k] == 'none'}
-
attributes[:custom_field_values].reject! {|k,v| v.blank?} if attributes[:custom_field_values]
-
attributes
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TrackersController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :index
-
1
before_filter :require_admin_or_api_request, :only => :index
-
1
accept_api_auth :index
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
-
render :action => "index", :layout => false if request.xhr?
-
}
-
format.api {
-
@trackers = Tracker.all
-
}
-
end
-
end
-
-
1
def new
-
@tracker ||= Tracker.new(params[:tracker])
-
@trackers = Tracker.find :all, :order => 'position'
-
@projects = Project.find(:all)
-
end
-
-
1
def create
-
@tracker = Tracker.new(params[:tracker])
-
if request.post? and @tracker.save
-
# workflow copy
-
if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
-
@tracker.workflows.copy(copy_from)
-
end
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to :action => 'index'
-
return
-
end
-
new
-
render :action => 'new'
-
end
-
-
1
def edit
-
@tracker ||= Tracker.find(params[:id])
-
@projects = Project.find(:all)
-
end
-
-
1
def update
-
@tracker = Tracker.find(params[:id])
-
if request.put? and @tracker.update_attributes(params[:tracker])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'index'
-
return
-
end
-
edit
-
render :action => 'edit'
-
end
-
-
1
def destroy
-
@tracker = Tracker.find(params[:id])
-
unless @tracker.issues.empty?
-
flash[:error] = l(:error_can_not_delete_tracker)
-
else
-
@tracker.destroy
-
end
-
redirect_to :action => 'index'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class UsersController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin, :except => :show
-
1
before_filter :find_user, :only => [:show, :edit, :update, :destroy, :edit_membership, :destroy_membership]
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
helper :sort
-
1
include SortHelper
-
1
helper :custom_fields
-
1
include CustomFieldsHelper
-
-
1
def index
-
sort_init 'login', 'asc'
-
sort_update %w(login firstname lastname mail admin created_on last_login_on)
-
-
case params[:format]
-
when 'xml', 'json'
-
@offset, @limit = api_offset_and_limit
-
else
-
@limit = per_page_option
-
end
-
-
@status = params[:status] || 1
-
-
scope = User.logged.status(@status)
-
scope = scope.like(params[:name]) if params[:name].present?
-
scope = scope.in_group(params[:group_id]) if params[:group_id].present?
-
-
@user_count = scope.count
-
@user_pages = Paginator.new self, @user_count, @limit, params['page']
-
@offset ||= @user_pages.current.offset
-
@users = scope.find :all,
-
:order => sort_clause,
-
:limit => @limit,
-
:offset => @offset
-
-
respond_to do |format|
-
format.html {
-
@groups = Group.all.sort
-
render :layout => !request.xhr?
-
}
-
format.api
-
end
-
end
-
-
1
def show
-
# show projects based on current user visibility
-
@memberships = @user.memberships.all(:conditions => Project.visible_condition(User.current))
-
-
events = Redmine::Activity::Fetcher.new(User.current, :author => @user).events(nil, nil, :limit => 10)
-
@events_by_day = events.group_by(&:event_date)
-
-
unless User.current.admin?
-
if !@user.active? || (@user != User.current && @memberships.empty? && events.empty?)
-
render_404
-
return
-
end
-
end
-
-
respond_to do |format|
-
format.html { render :layout => 'base' }
-
format.api
-
end
-
end
-
-
1
def new
-
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
-
@auth_sources = AuthSource.find(:all)
-
end
-
-
1
def create
-
@user = User.new(:language => Setting.default_language, :mail_notification => Setting.default_notification_option)
-
@user.safe_attributes = params[:user]
-
@user.admin = params[:user][:admin] || false
-
@user.login = params[:user][:login]
-
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation] unless @user.auth_source_id
-
-
if @user.save
-
@user.pref.attributes = params[:pref]
-
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
-
@user.pref.save
-
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
-
-
Mailer.account_information(@user, params[:user][:password]).deliver if params[:send_information]
-
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_create)
-
redirect_to(params[:continue] ?
-
{:controller => 'users', :action => 'new'} :
-
{:controller => 'users', :action => 'edit', :id => @user}
-
)
-
}
-
format.api { render :action => 'show', :status => :created, :location => user_url(@user) }
-
end
-
else
-
@auth_sources = AuthSource.find(:all)
-
# Clear password input
-
@user.password = @user.password_confirmation = nil
-
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.api { render_validation_errors(@user) }
-
end
-
end
-
end
-
-
1
def edit
-
@auth_sources = AuthSource.find(:all)
-
@membership ||= Member.new
-
end
-
-
1
def update
-
@user.admin = params[:user][:admin] if params[:user][:admin]
-
@user.login = params[:user][:login] if params[:user][:login]
-
if params[:user][:password].present? && (@user.auth_source_id.nil? || params[:user][:auth_source_id].blank?)
-
@user.password, @user.password_confirmation = params[:user][:password], params[:user][:password_confirmation]
-
end
-
@user.safe_attributes = params[:user]
-
# Was the account actived ? (do it before User#save clears the change)
-
was_activated = (@user.status_change == [User::STATUS_REGISTERED, User::STATUS_ACTIVE])
-
# TODO: Similar to My#account
-
@user.pref.attributes = params[:pref]
-
@user.pref[:no_self_notified] = (params[:no_self_notified] == '1')
-
-
if @user.save
-
@user.pref.save
-
@user.notified_project_ids = (@user.mail_notification == 'selected' ? params[:notified_project_ids] : [])
-
-
if was_activated
-
Mailer.account_activated(@user).deliver
-
elsif @user.active? && params[:send_information] && !params[:user][:password].blank? && @user.auth_source_id.nil?
-
Mailer.account_information(@user, params[:user][:password]).deliver
-
end
-
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to_referer_or edit_user_path(@user)
-
}
-
format.api { head :ok }
-
end
-
else
-
@auth_sources = AuthSource.find(:all)
-
@membership ||= Member.new
-
# Clear password input
-
@user.password = @user.password_confirmation = nil
-
-
respond_to do |format|
-
format.html { render :action => :edit }
-
format.api { render_validation_errors(@user) }
-
end
-
end
-
end
-
-
1
def destroy
-
@user.destroy
-
respond_to do |format|
-
format.html { redirect_to_referer_or(users_url) }
-
format.api { head :ok }
-
end
-
end
-
-
1
def edit_membership
-
@membership = Member.edit_membership(params[:membership_id], params[:membership], @user)
-
@membership.save
-
respond_to do |format|
-
if @membership.valid?
-
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
-
format.js {
-
render(:update) {|page|
-
page.replace_html "tab-content-memberships", :partial => 'users/memberships'
-
page.visual_effect(:highlight, "member-#{@membership.id}")
-
}
-
}
-
else
-
format.js {
-
render(:update) {|page|
-
page.alert(l(:notice_failed_to_save_members, :errors => @membership.errors.full_messages.join(', ')))
-
}
-
}
-
end
-
end
-
end
-
-
1
def destroy_membership
-
@membership = Member.find(params[:membership_id])
-
if @membership.deletable?
-
@membership.destroy
-
end
-
respond_to do |format|
-
format.html { redirect_to :controller => 'users', :action => 'edit', :id => @user, :tab => 'memberships' }
-
format.js { render(:update) {|page| page.replace_html "tab-content-memberships", :partial => 'users/memberships'} }
-
end
-
end
-
-
1
private
-
-
1
def find_user
-
if params[:id] == 'current'
-
require_login || return
-
@user = User.current
-
else
-
@user = User.find(params[:id])
-
end
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class VersionsController < ApplicationController
-
1
menu_item :roadmap
-
1
model_object Version
-
1
before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
-
1
before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
-
1
before_filter :find_project, :only => [:index, :new, :create, :close_completed]
-
1
before_filter :authorize
-
-
1
accept_api_auth :index, :show, :create, :update, :destroy
-
-
1
helper :custom_fields
-
1
helper :projects
-
-
1
def index
-
respond_to do |format|
-
format.html {
-
@trackers = @project.trackers.find(:all, :order => 'position')
-
retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
-
@with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
-
project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
-
-
@versions = @project.shared_versions || []
-
@versions += @project.rolled_up_versions.visible if @with_subprojects
-
@versions = @versions.uniq.sort
-
unless params[:completed]
-
@completed_versions = @versions.select {|version| version.closed? || version.completed? }
-
@versions -= @completed_versions
-
end
-
-
@issues_by_version = {}
-
if @selected_tracker_ids.any? && @versions.any?
-
issues = Issue.visible.all(
-
:include => [:project, :status, :tracker, :priority, :fixed_version],
-
:conditions => {:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)},
-
:order => "#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id"
-
)
-
@issues_by_version = issues.group_by(&:fixed_version)
-
end
-
@versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
-
}
-
format.api {
-
@versions = @project.shared_versions.all
-
}
-
end
-
end
-
-
1
def show
-
respond_to do |format|
-
format.html {
-
@issues = @version.fixed_issues.visible.find(:all,
-
:include => [:status, :tracker, :priority],
-
:order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
-
}
-
format.api
-
end
-
end
-
-
1
def new
-
@version = @project.versions.build
-
@version.safe_attributes = params[:version]
-
-
respond_to do |format|
-
format.html
-
format.js do
-
render :update do |page|
-
page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
-
page << "showModal('ajax-modal', '600px');"
-
page << "Form.Element.focus('version_name');"
-
end
-
end
-
end
-
end
-
-
1
def create
-
@version = @project.versions.build
-
if params[:version]
-
attributes = params[:version].dup
-
attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
-
@version.safe_attributes = attributes
-
end
-
-
if request.post?
-
if @version.save
-
respond_to do |format|
-
format.html do
-
flash[:notice] = l(:notice_successful_create)
-
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
end
-
format.js do
-
render(:update) {|page|
-
page << 'hideModal();'
-
# IE doesn't support the replace_html rjs method for select box options
-
page.replace "issue_fixed_version_id",
-
content_tag('select', content_tag('option') + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
-
}
-
end
-
format.api do
-
render :action => 'show', :status => :created, :location => version_url(@version)
-
end
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'new' }
-
format.js do
-
render :update do |page|
-
page.replace_html 'ajax-modal', :partial => 'versions/new_modal'
-
page << "Form.Element.focus('version_name');"
-
end
-
end
-
format.api { render_validation_errors(@version) }
-
end
-
end
-
end
-
end
-
-
1
def edit
-
end
-
-
1
def update
-
if request.put? && params[:version]
-
attributes = params[:version].dup
-
attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
-
@version.safe_attributes = attributes
-
if @version.save
-
respond_to do |format|
-
format.html {
-
flash[:notice] = l(:notice_successful_update)
-
redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
}
-
format.api { head :ok }
-
end
-
else
-
respond_to do |format|
-
format.html { render :action => 'edit' }
-
format.api { render_validation_errors(@version) }
-
end
-
end
-
end
-
end
-
-
1
def close_completed
-
if request.put?
-
@project.close_completed_versions
-
end
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
end
-
-
1
def destroy
-
if @version.fixed_issues.empty?
-
@version.destroy
-
respond_to do |format|
-
format.html { redirect_back_or_default :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project }
-
format.api { head :ok }
-
end
-
else
-
respond_to do |format|
-
format.html {
-
flash[:error] = l(:notice_unable_delete_version)
-
redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
-
}
-
format.api { head :unprocessable_entity }
-
end
-
end
-
end
-
-
1
def status_by
-
respond_to do |format|
-
format.html { render :action => 'show' }
-
format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
-
end
-
end
-
-
1
private
-
1
def find_project
-
@project = Project.find(params[:project_id])
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
1
def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
-
if ids = params[:tracker_ids]
-
@selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
-
else
-
@selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
-
end
-
end
-
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WatchersController < ApplicationController
-
1
before_filter :find_project
-
1
before_filter :require_login, :check_project_privacy, :only => [:watch, :unwatch]
-
1
before_filter :authorize, :only => [:new, :destroy]
-
-
1
def watch
-
if @watched.respond_to?(:visible?) && !@watched.visible?(User.current)
-
render_403
-
else
-
set_watcher(User.current, true)
-
end
-
end
-
-
1
def unwatch
-
set_watcher(User.current, false)
-
end
-
-
1
def new
-
respond_to do |format|
-
format.js do
-
render :update do |page|
-
page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
-
page << "showModal('ajax-modal', '400px');"
-
page << "$('ajax-modal').addClassName('new-watcher');"
-
end
-
end
-
end
-
end
-
-
1
def create
-
if params[:watcher].is_a?(Hash) && request.post?
-
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
-
user_ids.each do |user_id|
-
Watcher.create(:watchable => @watched, :user_id => user_id)
-
end
-
end
-
respond_to do |format|
-
format.html { redirect_to_referer_or {render :text => 'Watcher added.', :layout => true}}
-
format.js do
-
render :update do |page|
-
page.replace_html 'ajax-modal', :partial => 'watchers/new', :locals => {:watched => @watched}
-
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
-
end
-
end
-
end
-
end
-
-
1
def append
-
if params[:watcher].is_a?(Hash)
-
user_ids = params[:watcher][:user_ids] || [params[:watcher][:user_id]]
-
users = User.active.find_all_by_id(user_ids)
-
respond_to do |format|
-
format.js do
-
render :update do |page|
-
users.each do |user|
-
page << %|$$("#issue_watcher_user_ids_#{user.id}").each(function(el){el.remove();});|
-
end
-
page.insert_html :bottom, 'watchers_inputs', :text => watchers_checkboxes(nil, users, true)
-
end
-
end
-
end
-
end
-
end
-
-
1
def destroy
-
@watched.set_watcher(User.find(params[:user_id]), false) if request.post?
-
respond_to do |format|
-
format.html { redirect_to :back }
-
format.js do
-
render :update do |page|
-
page.replace_html 'watchers', :partial => 'watchers/watchers', :locals => {:watched => @watched}
-
end
-
end
-
end
-
end
-
-
1
def autocomplete_for_user
-
@users = User.active.like(params[:q]).find(:all, :limit => 100)
-
if @watched
-
@users -= @watched.watcher_users
-
end
-
render :layout => false
-
end
-
-
1
private
-
1
def find_project
-
if params[:object_type] && params[:object_id]
-
klass = Object.const_get(params[:object_type].camelcase)
-
return false unless klass.respond_to?('watched_by')
-
@watched = klass.find(params[:object_id])
-
@project = @watched.project
-
elsif params[:project_id]
-
@project = Project.visible.find_by_param(params[:project_id])
-
end
-
rescue
-
render_404
-
end
-
-
1
def set_watcher(user, watching)
-
@watched.set_watcher(user, watching)
-
respond_to do |format|
-
format.html { redirect_to_referer_or {render :text => (watching ? 'Watcher added.' : 'Watcher removed.'), :layout => true}}
-
format.js do
-
render(:update) do |page|
-
c = watcher_css(@watched)
-
page << %|$$(".#{c}").each(function(el){el.innerHTML="#{escape_javascript watcher_link(@watched, user)}"});|
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WelcomeController < ApplicationController
-
1
caches_action :robots
-
-
1
def index
-
4
@news = News.latest User.current
-
4
@projects = Project.latest User.current
-
end
-
-
1
def robots
-
@projects = Project.all_public.active
-
render :layout => false, :content_type => 'text/plain'
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'diff'
-
-
# The WikiController follows the Rails REST controller pattern but with
-
# a few differences
-
#
-
# * index - shows a list of WikiPages grouped by page or date
-
# * new - not used
-
# * create - not used
-
# * show - will also show the form for creating a new wiki page
-
# * edit - used to edit an existing or new page
-
# * update - used to save a wiki page update to the database, including new pages
-
# * destroy - normal
-
#
-
# Other member and collection methods are also used
-
#
-
# TODO: still being worked on
-
1
class WikiController < ApplicationController
-
1
default_search_scope :wiki_pages
-
1
before_filter :find_wiki, :authorize
-
1
before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
-
1
before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
-
-
1
helper :attachments
-
1
include AttachmentsHelper
-
1
helper :watchers
-
1
include Redmine::Export::PDF
-
-
# List of pages, sorted alphabetically and by parent (hierarchy)
-
1
def index
-
2
load_pages_for_index
-
2
@pages_by_parent_id = @pages.group_by(&:parent_id)
-
end
-
-
# List of page, by last update
-
1
def date_index
-
load_pages_for_index
-
@pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
-
end
-
-
# display a page (in editing mode if it doesn't exist)
-
1
def show
-
if @page.new_record?
-
if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
-
edit
-
render :action => 'edit'
-
else
-
render_404
-
end
-
return
-
end
-
if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
-
# Redirects user to the current version if he's not allowed to view previous versions
-
redirect_to :version => nil
-
return
-
end
-
@content = @page.content_for_version(params[:version])
-
if User.current.allowed_to?(:export_wiki_pages, @project)
-
if params[:format] == 'pdf'
-
send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
-
return
-
elsif params[:format] == 'html'
-
export = render_to_string :action => 'export', :layout => false
-
send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
-
return
-
elsif params[:format] == 'txt'
-
send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
-
return
-
end
-
end
-
@editable = editable?
-
@sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
-
@content.current_version? &&
-
Redmine::WikiFormatting.supports_section_edit?
-
-
render :action => 'show'
-
end
-
-
# edit an existing page or a new one
-
1
def edit
-
return render_403 unless editable?
-
if @page.new_record?
-
@page.content = WikiContent.new(:page => @page)
-
if params[:parent].present?
-
@page.parent = @page.wiki.find_page(params[:parent].to_s)
-
end
-
end
-
-
@content = @page.content_for_version(params[:version])
-
@content.text = initial_page_content(@page) if @content.text.blank?
-
# don't keep previous comment
-
@content.comments = nil
-
-
# To prevent StaleObjectError exception when reverting to a previous version
-
@content.version = @page.content.version
-
-
@text = @content.text
-
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
-
@section = params[:section].to_i
-
@text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
-
render_404 if @text.blank?
-
end
-
end
-
-
# Creates a new page or updates an existing one
-
1
def update
-
return render_403 unless editable?
-
@page.content = WikiContent.new(:page => @page) if @page.new_record?
-
@page.safe_attributes = params[:wiki_page]
-
-
@content = @page.content_for_version(params[:version])
-
@content.text = initial_page_content(@page) if @content.text.blank?
-
# don't keep previous comment
-
@content.comments = nil
-
-
if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
-
attachments = Attachment.attach_files(@page, params[:attachments])
-
render_attachment_warning_if_needed(@page)
-
# don't save content if text wasn't changed
-
@page.save
-
redirect_to :action => 'show', :project_id => @project, :id => @page.title
-
return
-
end
-
-
@content.comments = params[:content][:comments]
-
@text = params[:content][:text]
-
if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
-
@section = params[:section].to_i
-
@section_hash = params[:section_hash]
-
@content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
-
else
-
@content.version = params[:content][:version]
-
@content.text = @text
-
end
-
@content.author = User.current
-
@page.content = @content
-
if @page.save
-
attachments = Attachment.attach_files(@page, params[:attachments])
-
render_attachment_warning_if_needed(@page)
-
call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
-
redirect_to :action => 'show', :project_id => @project, :id => @page.title
-
else
-
render :action => 'edit'
-
end
-
-
rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
-
# Optimistic locking exception
-
flash.now[:error] = l(:notice_locking_conflict)
-
render :action => 'edit'
-
rescue ActiveRecord::RecordNotSaved
-
render :action => 'edit'
-
end
-
-
# rename a page
-
1
def rename
-
return render_403 unless editable?
-
@page.redirect_existing_links = true
-
# used to display the *original* title if some AR validation errors occur
-
@original_title = @page.pretty_title
-
if request.post? && @page.update_attributes(params[:wiki_page])
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'show', :project_id => @project, :id => @page.title
-
end
-
end
-
-
1
def protect
-
@page.update_attribute :protected, params[:protected]
-
redirect_to :action => 'show', :project_id => @project, :id => @page.title
-
end
-
-
# show page history
-
1
def history
-
@version_count = @page.content.versions.count
-
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
-
# don't load text
-
@versions = @page.content.versions.find :all,
-
:select => "id, author_id, comments, updated_on, version",
-
:order => 'version DESC',
-
:limit => @version_pages.items_per_page + 1,
-
:offset => @version_pages.current.offset
-
-
render :layout => false if request.xhr?
-
end
-
-
1
def diff
-
@diff = @page.diff(params[:version], params[:version_from])
-
render_404 unless @diff
-
end
-
-
1
def annotate
-
@annotate = @page.annotate(params[:version])
-
render_404 unless @annotate
-
end
-
-
# Removes a wiki page and its history
-
# Children can be either set as root pages, removed or reassigned to another parent page
-
1
def destroy
-
return render_403 unless editable?
-
-
@descendants_count = @page.descendants.size
-
if @descendants_count > 0
-
case params[:todo]
-
when 'nullify'
-
# Nothing to do
-
when 'destroy'
-
# Removes all its descendants
-
@page.descendants.each(&:destroy)
-
when 'reassign'
-
# Reassign children to another parent page
-
reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
-
return unless reassign_to
-
@page.children.each do |child|
-
child.update_attribute(:parent, reassign_to)
-
end
-
else
-
@reassignable_to = @wiki.pages - @page.self_and_descendants
-
return
-
end
-
end
-
@page.destroy
-
redirect_to :action => 'index', :project_id => @project
-
end
-
-
# Export wiki to a single pdf or html file
-
1
def export
-
@pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
-
respond_to do |format|
-
format.html {
-
export = render_to_string :action => 'export_multiple', :layout => false
-
send_data(export, :type => 'text/html', :filename => "wiki.html")
-
}
-
format.pdf {
-
send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
-
}
-
end
-
end
-
-
1
def preview
-
page = @wiki.find_page(params[:id])
-
# page is nil when previewing a new page
-
return render_403 unless page.nil? || editable?(page)
-
if page
-
@attachements = page.attachments
-
@previewed = page.content
-
end
-
@text = params[:content][:text]
-
render :partial => 'common/preview'
-
end
-
-
1
def add_attachment
-
return render_403 unless editable?
-
attachments = Attachment.attach_files(@page, params[:attachments])
-
render_attachment_warning_if_needed(@page)
-
redirect_to :action => 'show', :id => @page.title, :project_id => @project
-
end
-
-
1
private
-
-
1
def find_wiki
-
2
@project = Project.find(params[:project_id])
-
2
@wiki = @project.wiki
-
2
render_404 unless @wiki
-
rescue ActiveRecord::RecordNotFound
-
render_404
-
end
-
-
# Finds the requested page or a new page if it doesn't exist
-
1
def find_existing_or_new_page
-
@page = @wiki.find_or_new_page(params[:id])
-
if @wiki.page_found_with_redirect?
-
redirect_to params.update(:id => @page.title)
-
end
-
end
-
-
# Finds the requested page and returns a 404 error if it doesn't exist
-
1
def find_existing_page
-
@page = @wiki.find_page(params[:id])
-
if @page.nil?
-
render_404
-
return
-
end
-
if @wiki.page_found_with_redirect?
-
redirect_to params.update(:id => @page.title)
-
end
-
end
-
-
# Returns true if the current user is allowed to edit the page, otherwise false
-
1
def editable?(page = @page)
-
page.editable_by?(User.current)
-
end
-
-
# Returns the default content of a new wiki page
-
1
def initial_page_content(page)
-
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
-
extend helper unless self.instance_of?(helper)
-
helper.instance_method(:initial_page_content).bind(self).call(page)
-
end
-
-
1
def load_pages_for_index
-
2
@pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WikisController < ApplicationController
-
1
menu_item :settings
-
1
before_filter :find_project, :authorize
-
-
# Create or update a project's wiki
-
1
def edit
-
@wiki = @project.wiki || Wiki.new(:project => @project)
-
@wiki.safe_attributes = params[:wiki]
-
@wiki.save if request.post?
-
render(:update) {|page| page.replace_html "tab-content-wiki", :partial => 'projects/settings/wiki'}
-
end
-
-
# Delete a project's wiki
-
1
def destroy
-
if request.post? && params[:confirm] && @project.wiki
-
@project.wiki.destroy
-
redirect_to :controller => 'projects', :action => 'settings', :id => @project, :tab => 'wiki'
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WorkflowsController < ApplicationController
-
1
layout 'admin'
-
-
1
before_filter :require_admin
-
1
before_filter :find_roles
-
1
before_filter :find_trackers
-
-
1
def index
-
@workflow_counts = Workflow.count_by_tracker_and_role
-
end
-
-
1
def edit
-
@role = Role.find_by_id(params[:role_id])
-
@tracker = Tracker.find_by_id(params[:tracker_id])
-
-
if request.post?
-
Workflow.destroy_all( ["role_id=? and tracker_id=?", @role.id, @tracker.id])
-
(params[:issue_status] || []).each { |status_id, transitions|
-
transitions.each { |new_status_id, options|
-
author = options.is_a?(Array) && options.include?('author') && !options.include?('always')
-
assignee = options.is_a?(Array) && options.include?('assignee') && !options.include?('always')
-
@role.workflows.build(:tracker_id => @tracker.id, :old_status_id => status_id, :new_status_id => new_status_id, :author => author, :assignee => assignee)
-
}
-
}
-
if @role.save
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'edit', :role_id => @role, :tracker_id => @tracker
-
return
-
end
-
end
-
-
@used_statuses_only = (params[:used_statuses_only] == '0' ? false : true)
-
if @tracker && @used_statuses_only && @tracker.issue_statuses.any?
-
@statuses = @tracker.issue_statuses
-
end
-
@statuses ||= IssueStatus.find(:all, :order => 'position')
-
-
if @tracker && @role && @statuses.any?
-
workflows = Workflow.all(:conditions => {:role_id => @role.id, :tracker_id => @tracker.id})
-
@workflows = {}
-
@workflows['always'] = workflows.select {|w| !w.author && !w.assignee}
-
@workflows['author'] = workflows.select {|w| w.author}
-
@workflows['assignee'] = workflows.select {|w| w.assignee}
-
end
-
end
-
-
1
def copy
-
-
if params[:source_tracker_id].blank? || params[:source_tracker_id] == 'any'
-
@source_tracker = nil
-
else
-
@source_tracker = Tracker.find_by_id(params[:source_tracker_id].to_i)
-
end
-
if params[:source_role_id].blank? || params[:source_role_id] == 'any'
-
@source_role = nil
-
else
-
@source_role = Role.find_by_id(params[:source_role_id].to_i)
-
end
-
-
@target_trackers = params[:target_tracker_ids].blank? ? nil : Tracker.find_all_by_id(params[:target_tracker_ids])
-
@target_roles = params[:target_role_ids].blank? ? nil : Role.find_all_by_id(params[:target_role_ids])
-
-
if request.post?
-
if params[:source_tracker_id].blank? || params[:source_role_id].blank? || (@source_tracker.nil? && @source_role.nil?)
-
flash.now[:error] = l(:error_workflow_copy_source)
-
elsif @target_trackers.nil? || @target_roles.nil?
-
flash.now[:error] = l(:error_workflow_copy_target)
-
else
-
Workflow.copy(@source_tracker, @source_role, @target_trackers, @target_roles)
-
flash[:notice] = l(:notice_successful_update)
-
redirect_to :action => 'copy', :source_tracker_id => @source_tracker, :source_role_id => @source_role
-
end
-
end
-
end
-
-
1
private
-
-
1
def find_roles
-
@roles = Role.find(:all, :order => 'builtin, position')
-
end
-
-
1
def find_trackers
-
@trackers = Tracker.find(:all, :order => 'position')
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AccountHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AdminHelper
-
1
def project_status_options_for_select(selected)
-
options_for_select([[l(:label_all), ''],
-
[l(:status_active), '1']], selected.to_s)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'forwardable'
-
1
require 'cgi'
-
-
1
module ApplicationHelper
-
1
include Redmine::WikiFormatting::Macros::Definitions
-
1
include Redmine::I18n
-
1
include GravatarHelper::PublicMethods
-
-
1
extend Forwardable
-
1
def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
-
-
# Return true if user is authorized for controller/action, otherwise false
-
1
def authorize_for(controller, action)
-
76
User.current.allowed_to?({:controller => controller, :action => action}, @project)
-
end
-
-
# Display a link if user is authorized
-
#
-
# @param [String] name Anchor text (passed to link_to)
-
# @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
-
# @param [optional, Hash] html_options Options passed to link_to
-
# @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
-
1
def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
-
22
link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
-
end
-
-
# Display a link to remote if user is authorized
-
1
def link_to_remote_if_authorized(name, options = {}, html_options = nil)
-
url = options[:url] || {}
-
link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
-
end
-
-
# Displays a link to user's account page if active
-
1
def link_to_user(user, options={})
-
475
if user.is_a?(User)
-
473
name = h(user.name(options[:format]))
-
473
if user.active?
-
473
link_to name, :controller => 'users', :action => 'show', :id => user
-
else
-
name
-
end
-
else
-
2
h(user.to_s)
-
end
-
end
-
-
# Displays a link to +issue+ with its subject.
-
# Examples:
-
#
-
# link_to_issue(issue) # => Defect #6: This is the subject
-
# link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
-
# link_to_issue(issue, :subject => false) # => Defect #6
-
# link_to_issue(issue, :project => true) # => Foo - Defect #6
-
#
-
1
def link_to_issue(issue, options={})
-
7
title = nil
-
7
subject = nil
-
7
if options[:subject] == false
-
2
title = truncate(issue.subject, :length => 60)
-
else
-
5
subject = issue.subject
-
5
if options[:truncate]
-
5
subject = truncate(subject, :length => options[:truncate])
-
end
-
end
-
7
s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
-
:class => issue.css_classes,
-
:title => title
-
7
s << h(": #{subject}") if subject
-
7
s = h("#{issue.project} - ") + s if options[:project]
-
7
s
-
end
-
-
# Generates a link to an attachment.
-
# Options:
-
# * :text - Link text (default to attachment filename)
-
# * :download - Force download (default: false)
-
1
def link_to_attachment(attachment, options={})
-
text = options.delete(:text) || attachment.filename
-
action = options.delete(:download) ? 'download' : 'show'
-
opt_only_path = {}
-
opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
-
options.delete(:only_path)
-
link_to(h(text),
-
{:controller => 'attachments', :action => action,
-
:id => attachment, :filename => attachment.filename}.merge(opt_only_path),
-
options)
-
end
-
-
# Generates a link to a SCM revision
-
# Options:
-
# * :text - Link text (default to the formatted revision)
-
1
def link_to_revision(revision, repository, options={})
-
if repository.is_a?(Project)
-
repository = repository.repository
-
end
-
text = options.delete(:text) || format_revision(revision)
-
rev = revision.respond_to?(:identifier) ? revision.identifier : revision
-
link_to(
-
h(text),
-
{:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
-
:title => l(:label_revision_id, format_revision(revision))
-
)
-
end
-
-
# Generates a link to a message
-
1
def link_to_message(message, options={}, html_options = nil)
-
link_to(
-
h(truncate(message.subject, :length => 60)),
-
{ :controller => 'messages', :action => 'show',
-
:board_id => message.board_id,
-
:id => message.root,
-
:r => (message.parent_id && message.id),
-
:anchor => (message.parent_id ? "message-#{message.id}" : nil)
-
}.merge(options),
-
html_options
-
)
-
end
-
-
# Generates a link to a project if active
-
# Examples:
-
#
-
# link_to_project(project) # => link to the specified project overview
-
# link_to_project(project, :action=>'settings') # => link to project settings
-
# link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
-
# link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
-
#
-
1
def link_to_project(project, options={}, html_options = nil)
-
514
if project.active?
-
514
url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
-
514
link_to(h(project), url, html_options)
-
else
-
h(project)
-
end
-
end
-
-
1
def toggle_link(name, id, options={})
-
4
onclick = "Element.toggle('#{id}'); "
-
4
onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
-
4
onclick << "return false;"
-
4
link_to(name, "#", :onclick => onclick)
-
end
-
-
1
def image_to_function(name, function, html_options = {})
-
html_options.symbolize_keys!
-
tag(:input, html_options.merge({
-
:type => "image", :src => image_path(name),
-
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
-
}))
-
end
-
-
1
def prompt_to_remote(name, text, param, url, html_options = {})
-
4
html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
-
4
link_to name, {}, html_options
-
end
-
-
1
def format_activity_title(text)
-
h(truncate_single_line(text, :length => 100))
-
end
-
-
1
def format_activity_day(date)
-
date == User.current.today ? l(:label_today).titleize : format_date(date)
-
end
-
-
1
def format_activity_description(text)
-
h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
-
).gsub(/[\r\n]+/, "<br />").html_safe
-
end
-
-
1
def format_version_name(version)
-
10
if version.project == @project
-
10
h(version)
-
else
-
h("#{version.project} - #{version}")
-
end
-
end
-
-
1
def due_date_distance_in_words(date)
-
8
if date
-
8
l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
-
end
-
end
-
-
1
def render_page_hierarchy(pages, node=nil, options={})
-
6
content = ''
-
6
if pages[node]
-
6
content << "<ul class=\"pages-hierarchy\">\n"
-
6
pages[node].each do |page|
-
18
content << "<li>"
-
content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
-
18
:title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
-
18
content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
-
18
content << "</li>\n"
-
end
-
6
content << "</ul>\n"
-
end
-
6
content.html_safe
-
end
-
-
# Renders flash messages
-
1
def render_flash_messages
-
359
s = ''
-
359
flash.each do |k,v|
-
6
s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
-
end
-
359
s.html_safe
-
end
-
-
# Renders tabs and their content
-
1
def render_tabs(tabs)
-
if tabs.any?
-
render :partial => 'common/tabs', :locals => {:tabs => tabs}
-
else
-
content_tag 'p', l(:label_no_data), :class => "nodata"
-
end
-
end
-
-
# Renders the project quick-jump box
-
1
def render_project_jump_box
-
359
return unless User.current.logged?
-
232
projects = User.current.memberships.collect(&:project).compact.uniq
-
232
if projects.any?
-
232
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
-
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
-
'<option value="" disabled="disabled">---</option>'
-
s << project_tree_options_for_select(projects, :selected => @project) do |p|
-
764
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
-
232
end
-
232
s << '</select>'
-
232
s.html_safe
-
end
-
end
-
-
1
def project_tree_options_for_select(projects, options = {})
-
343
s = ''
-
343
project_tree(projects) do |project, level|
-
997
name_prefix = (level > 0 ? (' ' * 2 * level + '» ').html_safe : '')
-
997
tag_options = {:value => project.id}
-
997
if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
-
219
tag_options[:selected] = 'selected'
-
else
-
778
tag_options[:selected] = nil
-
end
-
997
tag_options.merge!(yield(project)) if block_given?
-
997
s << content_tag('option', name_prefix + h(project), tag_options)
-
end
-
343
s.html_safe
-
end
-
-
# Yields the given block for each project with its level in the tree
-
#
-
# Wrapper for Project#project_tree
-
1
def project_tree(projects, &block)
-
343
Project.project_tree(projects, &block)
-
end
-
-
1
def project_nested_ul(projects, &block)
-
s = ''
-
if projects.any?
-
ancestors = []
-
projects.sort_by(&:lft).each do |project|
-
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
-
s << "<ul>\n"
-
else
-
ancestors.pop
-
s << "</li>"
-
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
-
ancestors.pop
-
s << "</ul></li>\n"
-
end
-
end
-
s << "<li>"
-
s << yield(project).to_s
-
ancestors << project
-
end
-
s << ("</li></ul>\n" * ancestors.size)
-
end
-
s.html_safe
-
end
-
-
1
def principals_check_box_tags(name, principals)
-
s = ''
-
principals.sort.each do |principal|
-
s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
-
end
-
s.html_safe
-
end
-
-
# Returns a string for users/groups option tags
-
1
def principals_options_for_select(collection, selected=nil)
-
4
s = ''
-
4
if collection.include?(User.current)
-
4
s << content_tag('option', "<< #{l(:label_me)} >>".html_safe, :value => User.current.id)
-
end
-
4
groups = ''
-
4
collection.sort.each do |element|
-
8
selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
-
8
(element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
-
end
-
4
unless groups.empty?
-
s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
-
end
-
4
s.html_safe
-
end
-
-
# Truncates and returns the string as a single line
-
1
def truncate_single_line(string, *args)
-
truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
-
end
-
-
# Truncates at line break after 250 characters or options[:length]
-
1
def truncate_lines(string, options={})
-
length = options[:length] || 250
-
if string.to_s =~ /\A(.{#{length}}.*?)$/m
-
"#{$1}..."
-
else
-
string
-
end
-
end
-
-
1
def anchor(text)
-
text.to_s.gsub(' ', '_')
-
end
-
-
1
def html_hours(text)
-
8
text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
-
end
-
-
1
def authoring(created, author, options={})
-
110
l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
-
end
-
-
1
def time_tag(time)
-
110
text = distance_of_time_in_words(Time.now, time)
-
110
if @project
-
102
link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
-
else
-
8
content_tag('acronym', text, :title => format_time(time))
-
end
-
end
-
-
1
def syntax_highlight_lines(name, content)
-
lines = []
-
syntax_highlight(name, content).each_line { |line| lines << line }
-
lines
-
end
-
-
1
def syntax_highlight(name, content)
-
Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
-
end
-
-
1
def to_path_param(path)
-
str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
-
str.blank? ? nil : str
-
end
-
-
1
def pagination_links_full(paginator, count=nil, options={})
-
15
page_param = options.delete(:page_param) || :page
-
15
per_page_links = options.delete(:per_page_links)
-
15
url_param = params.dup
-
-
15
html = ''
-
15
if paginator.current.previous
-
# \xc2\xab(utf-8) = «
-
html << link_to_content_update(
-
"\xc2\xab " + l(:label_previous),
-
url_param.merge(page_param => paginator.current.previous)) + ' '
-
end
-
-
html << (pagination_links_each(paginator, options) do |n|
-
link_to_content_update(n.to_s, url_param.merge(page_param => n))
-
15
end || '')
-
-
15
if paginator.current.next
-
# \xc2\xbb(utf-8) = »
-
html << ' ' + link_to_content_update(
-
(l(:label_next) + " \xc2\xbb"),
-
url_param.merge(page_param => paginator.current.next))
-
end
-
-
15
unless count.nil?
-
15
html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
-
15
if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
-
html << " | #{links}"
-
end
-
end
-
-
15
html.html_safe
-
end
-
-
1
def per_page_links(selected=nil, item_count=nil)
-
15
values = Setting.per_page_options_array
-
15
if item_count && values.any?
-
15
if item_count > values.first
-
max = values.detect {|value| value >= item_count} || item_count
-
else
-
15
max = item_count
-
end
-
60
values = values.select {|value| value <= max || value == selected}
-
end
-
15
if values.empty? || (values.size == 1 && values.first == selected)
-
15
return nil
-
end
-
links = values.collect do |n|
-
n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
-
end
-
l(:label_display_per_page, links.join(', '))
-
end
-
-
1
def reorder_links(name, url, method = :post)
-
link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
-
url.merge({"#{name}[move_to]" => 'highest'}),
-
:method => method, :title => l(:label_sort_highest)) +
-
link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
-
url.merge({"#{name}[move_to]" => 'higher'}),
-
:method => method, :title => l(:label_sort_higher)) +
-
link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
-
url.merge({"#{name}[move_to]" => 'lower'}),
-
:method => method, :title => l(:label_sort_lower)) +
-
link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
-
url.merge({"#{name}[move_to]" => 'lowest'}),
-
:method => method, :title => l(:label_sort_lowest))
-
end
-
-
1
def breadcrumb(*args)
-
4
elements = args.flatten
-
4
elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
-
end
-
-
1
def other_formats_links(&block)
-
25
concat('<p class="other-formats">'.html_safe + l(:label_export_to))
-
25
yield Redmine::Views::OtherFormatsBuilder.new(self)
-
25
concat('</p>'.html_safe)
-
end
-
-
1
def page_header_title
-
359
if @project.nil? || @project.new_record?
-
251
h(Setting.app_title)
-
else
-
108
b = []
-
108
ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
-
108
if ancestors.any?
-
12
root = ancestors.shift
-
12
b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
-
12
if ancestors.size > 2
-
b << "\xe2\x80\xa6"
-
ancestors = ancestors[-2, 2]
-
end
-
13
b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
-
end
-
108
b << h(@project)
-
108
b.join(" \xc2\xbb ").html_safe
-
end
-
end
-
-
1
def html_title(*args)
-
686
if args.empty?
-
466
title = @html_title || []
-
466
title << @project.name if @project
-
466
title << Setting.app_title unless Setting.app_title == title.last
-
1371
title.select {|t| !t.blank? }.join(' - ')
-
else
-
220
@html_title ||= []
-
220
@html_title += args
-
end
-
end
-
-
# Returns the theme, controller name, and action as css classes for the
-
# HTML body.
-
1
def body_css_classes
-
359
css = []
-
359
if theme = Redmine::Themes.theme(Setting.ui_theme)
-
css << 'theme-' + theme.name
-
end
-
-
359
css << 'controller-' + controller_name
-
359
css << 'action-' + action_name
-
359
css.join(' ')
-
end
-
-
1
def accesskey(s)
-
734
Redmine::AccessKeys.key_for s
-
end
-
-
# Formats text according to system settings.
-
# 2 ways to call this method:
-
# * with a String: textilizable(text, options)
-
# * with an object and one of its attribute: textilizable(issue, :description, options)
-
1
def textilizable(*args)
-
1315
options = args.last.is_a?(Hash) ? args.pop : {}
-
1315
case args.size
-
when 1
-
87
obj = options[:object]
-
87
text = args.shift
-
when 2
-
1228
obj = args.shift
-
1228
attr = args.shift
-
1228
text = obj.send(attr).to_s
-
else
-
raise ArgumentError, 'invalid arguments to textilizable'
-
end
-
1315
return '' if text.blank?
-
73
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
-
73
only_path = options.delete(:only_path) == false ? false : true
-
-
73
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
-
-
73
@parsed_headings = []
-
73
@heading_anchors = {}
-
73
@current_section = 0 if options[:edit_section_links]
-
-
73
parse_sections(text, project, obj, attr, only_path, options)
-
73
text = parse_non_pre_blocks(text) do |text|
-
73
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_macros].each do |method_name|
-
292
send method_name, text, project, obj, attr, only_path, options
-
end
-
end
-
73
parse_headings(text, project, obj, attr, only_path, options)
-
-
73
if @parsed_headings.any?
-
replace_toc(text, @parsed_headings)
-
end
-
-
73
text.html_safe
-
end
-
-
1
def parse_non_pre_blocks(text)
-
73
s = StringScanner.new(text)
-
73
tags = []
-
73
parsed = ''
-
73
while !s.eos?
-
73
s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
-
73
text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
-
73
if tags.empty?
-
73
yield text
-
end
-
73
parsed << text
-
73
if tag
-
if closing
-
if tags.last == tag.downcase
-
tags.pop
-
end
-
else
-
tags << tag.downcase
-
end
-
parsed << full_tag
-
end
-
end
-
# Close any non closing tags
-
73
while tag = tags.pop
-
parsed << "</#{tag}>"
-
end
-
73
parsed
-
end
-
-
1
def parse_inline_attachments(text, project, obj, attr, only_path, options)
-
# when using an image link, try to use an attachment, if possible
-
73
if options[:attachments] || (obj && obj.respond_to?(:attachments))
-
attachments = options[:attachments] || obj.attachments
-
text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
-
filename, ext, alt, alttext = $1.downcase, $2, $3, $4
-
# search for the picture in attachments
-
if found = Attachment.latest_attach(attachments, filename)
-
image_url = url_for :only_path => only_path, :controller => 'attachments',
-
:action => 'download', :id => found
-
desc = found.description.to_s.gsub('"', '')
-
if !desc.blank? && alttext.blank?
-
alt = " title=\"#{desc}\" alt=\"#{desc}\""
-
end
-
"src=\"#{image_url}\"#{alt}"
-
else
-
m
-
end
-
end
-
end
-
end
-
-
# Wiki links
-
#
-
# Examples:
-
# [[mypage]]
-
# [[mypage|mytext]]
-
# wiki links can refer other project wikis, using project name or identifier:
-
# [[project:]] -> wiki starting page
-
# [[project:|mytext]]
-
# [[project:mypage]]
-
# [[project:mypage|mytext]]
-
1
def parse_wiki_links(text, project, obj, attr, only_path, options)
-
73
text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
-
link_project = project
-
esc, all, page, title = $1, $2, $3, $5
-
if esc.nil?
-
if page =~ /^([^\:]+)\:(.*)$/
-
link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
-
page = $2
-
title ||= $1 if page.blank?
-
end
-
-
if link_project && link_project.wiki
-
# extract anchor
-
anchor = nil
-
if page =~ /^(.+?)\#(.+)$/
-
page, anchor = $1, $2
-
end
-
anchor = sanitize_anchor_name(anchor) if anchor.present?
-
# check if page exists
-
wiki_page = link_project.wiki.find_page(page)
-
url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
-
"##{anchor}"
-
else
-
case options[:wiki_links]
-
when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
-
when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
-
else
-
wiki_page_id = page.present? ? Wiki.titleize(page) : nil
-
parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
-
url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
-
:id => wiki_page_id, :anchor => anchor, :parent => parent)
-
end
-
end
-
link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
-
else
-
# project or wiki doesn't exist
-
all
-
end
-
else
-
all
-
end
-
end
-
end
-
-
# Redmine links
-
#
-
# Examples:
-
# Issues:
-
# #52 -> Link to issue #52
-
# Changesets:
-
# r52 -> Link to revision 52
-
# commit:a85130f -> Link to scmid starting with a85130f
-
# Documents:
-
# document#17 -> Link to document with id 17
-
# document:Greetings -> Link to the document with title "Greetings"
-
# document:"Some document" -> Link to the document with title "Some document"
-
# Versions:
-
# version#3 -> Link to version with id 3
-
# version:1.0.0 -> Link to version named "1.0.0"
-
# version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
-
# Attachments:
-
# attachment:file.zip -> Link to the attachment of the current object named file.zip
-
# Source files:
-
# source:some/file -> Link to the file located at /some/file in the project's repository
-
# source:some/file@52 -> Link to the file's revision 52
-
# source:some/file#L120 -> Link to line 120 of the file
-
# source:some/file@52#L120 -> Link to line 120 of the file's revision 52
-
# export:some/file -> Force the download of the file
-
# Forum messages:
-
# message#1218 -> Link to message with id 1218
-
#
-
# Links can refer other objects from other projects, using project identifier:
-
# identifier:r52
-
# identifier:document:"Some document"
-
# identifier:version:1.0.0
-
# identifier:source:some/file
-
1
def parse_redmine_links(text, project, obj, attr, only_path, options)
-
73
text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
-
leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
-
link = nil
-
if project_identifier
-
project = Project.visible.find_by_identifier(project_identifier)
-
end
-
if esc.nil?
-
if prefix.nil? && sep == 'r'
-
if project
-
repository = nil
-
if repo_identifier
-
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
-
else
-
repository = project.repository
-
end
-
# project.changesets.visible raises an SQL error because of a double join on repositories
-
if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
-
link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
-
:class => 'changeset',
-
:title => truncate_single_line(changeset.comments, :length => 100))
-
end
-
end
-
elsif sep == '#'
-
oid = identifier.to_i
-
case prefix
-
when nil
-
if issue = Issue.visible.find_by_id(oid, :include => :status)
-
anchor = comment_id ? "note-#{comment_id}" : nil
-
link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
-
:class => issue.css_classes,
-
:title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
-
end
-
when 'document'
-
if document = Document.visible.find_by_id(oid)
-
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
-
:class => 'document'
-
end
-
when 'version'
-
if version = Version.visible.find_by_id(oid)
-
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
-
:class => 'version'
-
end
-
when 'message'
-
if message = Message.visible.find_by_id(oid, :include => :parent)
-
link = link_to_message(message, {:only_path => only_path}, :class => 'message')
-
end
-
when 'forum'
-
if board = Board.visible.find_by_id(oid)
-
link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
-
:class => 'board'
-
end
-
when 'news'
-
if news = News.visible.find_by_id(oid)
-
link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
-
:class => 'news'
-
end
-
when 'project'
-
if p = Project.visible.find_by_id(oid)
-
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
-
end
-
end
-
elsif sep == ':'
-
# removes the double quotes if any
-
name = identifier.gsub(%r{^"(.*)"$}, "\\1")
-
case prefix
-
when 'document'
-
if project && document = project.documents.visible.find_by_title(name)
-
link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
-
:class => 'document'
-
end
-
when 'version'
-
if project && version = project.versions.visible.find_by_name(name)
-
link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
-
:class => 'version'
-
end
-
when 'forum'
-
if project && board = project.boards.visible.find_by_name(name)
-
link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
-
:class => 'board'
-
end
-
when 'news'
-
if project && news = project.news.visible.find_by_title(name)
-
link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
-
:class => 'news'
-
end
-
when 'commit', 'source', 'export'
-
if project
-
repository = nil
-
if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
-
repo_prefix, repo_identifier, name = $1, $2, $3
-
repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
-
else
-
repository = project.repository
-
end
-
if prefix == 'commit'
-
if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
-
link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
-
:class => 'changeset',
-
:title => truncate_single_line(h(changeset.comments), :length => 100)
-
end
-
else
-
if repository && User.current.allowed_to?(:browse_repository, project)
-
name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
-
path, rev, anchor = $1, $3, $5
-
link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
-
:path => to_path_param(path),
-
:rev => rev,
-
:anchor => anchor,
-
:format => (prefix == 'export' ? 'raw' : nil)},
-
:class => (prefix == 'export' ? 'source download' : 'source')
-
end
-
end
-
repo_prefix = nil
-
end
-
when 'attachment'
-
attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
-
if attachments && attachment = attachments.detect {|a| a.filename == name }
-
link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
-
:class => 'attachment'
-
end
-
when 'project'
-
if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
-
link = link_to_project(p, {:only_path => only_path}, :class => 'project')
-
end
-
end
-
end
-
end
-
(leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
-
end
-
end
-
-
1
HEADING_RE = /(<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>)/i unless const_defined?(:HEADING_RE)
-
-
1
def parse_sections(text, project, obj, attr, only_path, options)
-
73
return unless options[:edit_section_links]
-
text.gsub!(HEADING_RE) do
-
heading = $1
-
@current_section += 1
-
if @current_section > 1
-
content_tag('div',
-
link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
-
:class => 'contextual',
-
:title => l(:button_edit_section)) + heading.html_safe
-
else
-
heading
-
end
-
end
-
end
-
-
# Headings and TOC
-
# Adds ids and links to headings unless options[:headings] is set to false
-
1
def parse_headings(text, project, obj, attr, only_path, options)
-
73
return if options[:headings] == false
-
-
73
text.gsub!(HEADING_RE) do
-
level, attrs, content = $2.to_i, $3, $4
-
item = strip_tags(content).strip
-
anchor = sanitize_anchor_name(item)
-
# used for single-file wiki export
-
anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
-
@heading_anchors[anchor] ||= 0
-
idx = (@heading_anchors[anchor] += 1)
-
if idx > 1
-
anchor = "#{anchor}-#{idx}"
-
end
-
@parsed_headings << [level, anchor, item]
-
"<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>"
-
end
-
end
-
-
MACROS_RE = /
-
(!)? # escaping
-
(
-
\{\{ # opening tag
-
([\w]+) # macro name
-
(\(([^\}]*)\))? # optional arguments
-
\}\} # closing tag
-
)
-
1
/x unless const_defined?(:MACROS_RE)
-
-
# Macros substitution
-
1
def parse_macros(text, project, obj, attr, only_path, options)
-
73
text.gsub!(MACROS_RE) do
-
esc, all, macro = $1, $2, $3.downcase
-
args = ($5 || '').split(',').each(&:strip)
-
if esc.nil?
-
begin
-
exec_macro(macro, obj, args)
-
rescue => e
-
"<div class=\"flash error\">Error executing the <strong>#{macro}</strong> macro (#{e})</div>"
-
end || all
-
else
-
all
-
end
-
end
-
end
-
-
1
TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
-
-
# Renders the TOC with given headings
-
1
def replace_toc(text, headings)
-
text.gsub!(TOC_RE) do
-
if headings.empty?
-
''
-
else
-
div_class = 'toc'
-
div_class << ' right' if $1 == '>'
-
div_class << ' left' if $1 == '<'
-
out = "<ul class=\"#{div_class}\"><li>"
-
root = headings.map(&:first).min
-
current = root
-
started = false
-
headings.each do |level, anchor, item|
-
if level > current
-
out << '<ul><li>' * (level - current)
-
elsif level < current
-
out << "</li></ul>\n" * (current - level) + "</li><li>"
-
elsif started
-
out << '</li><li>'
-
end
-
out << "<a href=\"##{anchor}\">#{item}</a>"
-
current = level
-
started = true
-
end
-
out << '</li></ul>' * (current - root)
-
out << '</li></ul>'
-
end
-
end
-
end
-
-
# Same as Rails' simple_format helper without using paragraphs
-
1
def simple_format_without_paragraph(text)
-
text.to_s.
-
gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
-
gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
-
gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
-
html_safe
-
end
-
-
1
def lang_options_for_select(blank=true)
-
(blank ? [["(auto)", ""]] : []) +
-
valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
-
end
-
-
1
def label_tag_for(name, option_tags = nil, options = {})
-
label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
-
content_tag("label", label_text)
-
end
-
-
1
def labelled_tabular_form_for(*args, &proc)
-
ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_tabular_form_for is deprecated and will be removed in Redmine 1.5. Use #labelled_form_for instead."
-
args << {} unless args.last.is_a?(Hash)
-
options = args.last
-
options[:html] ||= {}
-
options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
-
options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
-
form_for(*args, &proc)
-
end
-
-
1
def labelled_form_for(*args, &proc)
-
8
args << {} unless args.last.is_a?(Hash)
-
8
options = args.last
-
8
if args.first.is_a?(Symbol)
-
1
options.merge!(:as => args.shift)
-
end
-
8
options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
-
8
form_for(*args, &proc)
-
end
-
-
1
def labelled_fields_for(*args, &proc)
-
12
args << {} unless args.last.is_a?(Hash)
-
12
options = args.last
-
12
options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
-
12
fields_for(*args, &proc)
-
end
-
-
1
def labelled_remote_form_for(*args, &proc)
-
args << {} unless args.last.is_a?(Hash)
-
options = args.last
-
options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
-
form_for(*args, &proc)
-
end
-
-
1
def error_messages_for(*objects)
-
24
html = ""
-
50
objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
-
48
errors = objects.map {|o| o.errors.full_messages}.flatten
-
24
if errors.any?
-
html << "<div id='errorExplanation'><ul>\n"
-
errors.each do |error|
-
html << "<li>#{h error}</li>\n"
-
end
-
html << "</ul></div>\n"
-
end
-
24
html.html_safe
-
end
-
-
1
def back_url_hidden_field_tag
-
125
back_url = params[:back_url] || request.env['HTTP_REFERER']
-
125
back_url = CGI.unescape(back_url.to_s)
-
125
hidden_field_tag('back_url', CGI.escape(back_url), :id => nil) unless back_url.blank?
-
end
-
-
1
def check_all_links(form_name)
-
link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
-
" | ".html_safe +
-
link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
-
end
-
-
1
def progress_bar(pcts, options={})
-
12
pcts = [pcts, pcts] unless pcts.is_a?(Array)
-
12
pcts = pcts.collect(&:round)
-
12
pcts[1] = pcts[1] - pcts[0]
-
12
pcts << (100 - pcts[1] - pcts[0])
-
12
width = options[:width] || '100px;'
-
12
legend = options[:legend] || ''
-
content_tag('table',
-
content_tag('tr',
-
12
(pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
-
12
(pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
-
12
(pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
-
), :class => 'progress', :style => "width: #{width};").html_safe +
-
12
content_tag('p', legend, :class => 'pourcent').html_safe
-
end
-
-
1
def checked_image(checked=true)
-
if checked
-
image_tag 'toggle_check.png'
-
end
-
end
-
-
1
def context_menu(url)
-
142
unless @context_menu_included
-
142
content_for :header_tags do
-
javascript_include_tag('context_menu') +
-
142
stylesheet_link_tag('context_menu')
-
end
-
142
if l(:direction) == 'rtl'
-
content_for :header_tags do
-
stylesheet_link_tag('context_menu_rtl')
-
end
-
end
-
142
@context_menu_included = true
-
end
-
142
javascript_tag "new ContextMenu('#{ url_for(url) }')"
-
end
-
-
1
def calendar_for(field_id)
-
160
include_calendar_headers_tags
-
image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
-
160
javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
-
end
-
-
1
def include_calendar_headers_tags
-
160
unless @calendar_headers_tags_included
-
25
@calendar_headers_tags_included = true
-
25
content_for :header_tags do
-
25
start_of_week = case Setting.start_of_week.to_i
-
when 1
-
'Calendar._FD = 1;' # Monday
-
when 7
-
'Calendar._FD = 0;' # Sunday
-
when 6
-
'Calendar._FD = 6;' # Saturday
-
else
-
25
'' # use language
-
end
-
-
javascript_include_tag('calendar/calendar') +
-
javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
-
javascript_tag(start_of_week) +
-
javascript_include_tag('calendar/calendar-setup') +
-
25
stylesheet_link_tag('calendar')
-
end
-
end
-
end
-
-
# Overrides Rails' stylesheet_link_tag with themes and plugins support.
-
# Examples:
-
# stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
-
# stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
-
#
-
1
def stylesheet_link_tag(*sources)
-
1584
options = sources.last.is_a?(Hash) ? sources.pop : {}
-
1584
plugin = options.delete(:plugin)
-
1584
sources = sources.map do |source|
-
1691
if plugin
-
1153
"/plugin_assets/#{plugin}/stylesheets/#{source}"
-
elsif current_theme && current_theme.stylesheets.include?(source)
-
current_theme.stylesheet_path(source)
-
else
-
538
source
-
end
-
end
-
1584
super sources, options
-
end
-
-
# Overrides Rails' image_tag with themes and plugins support.
-
# Examples:
-
# image_tag('image.png') # => picks image.png from the current theme or defaults
-
# image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
-
#
-
1
def image_tag(source, options={})
-
544
if plugin = options.delete(:plugin)
-
source = "/plugin_assets/#{plugin}/images/#{source}"
-
elsif current_theme && current_theme.images.include?(source)
-
source = current_theme.image_path(source)
-
end
-
544
super source, options
-
end
-
-
# Overrides Rails' javascript_include_tag with plugins support
-
# Examples:
-
# javascript_include_tag('scripts') # => picks scripts.js from defaults
-
# javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
-
#
-
1
def javascript_include_tag(*sources)
-
6306
options = sources.last.is_a?(Hash) ? sources.pop : {}
-
6306
if plugin = options.delete(:plugin)
-
5704
sources = sources.map do |source|
-
7429
if plugin
-
7429
"/plugin_assets/#{plugin}/javascripts/#{source}"
-
else
-
source
-
end
-
end
-
end
-
6306
super sources, options
-
end
-
-
1
def content_for(name, content = nil, &block)
-
1201
@has_content ||= {}
-
1201
@has_content[name] = true
-
1201
super(name, content, &block)
-
end
-
-
1
def has_content?(name)
-
359
(@has_content && @has_content[name]) || false
-
end
-
-
1
def sidebar_content?
-
359
has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
-
end
-
-
1
def view_layouts_base_sidebar_hook_response
-
623
@view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
-
end
-
-
1
def email_delivery_enabled?
-
!!ActionMailer::Base.perform_deliveries
-
end
-
-
# Returns the avatar image tag for the given +user+ if avatars are enabled
-
# +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
-
1
def avatar(user, options = { })
-
4
if Setting.gravatar_enabled?
-
options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
-
email = nil
-
if user.respond_to?(:mail)
-
email = user.mail
-
elsif user.to_s =~ %r{<(.+?)>}
-
email = $1
-
end
-
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
-
else
-
4
''
-
end
-
end
-
-
1
def sanitize_anchor_name(anchor)
-
anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
-
end
-
-
# Returns the javascript tags that are included in the html layout head
-
1
def javascript_heads
-
359
tags = javascript_include_tag('prototype', 'effects', 'dragdrop', 'controls', 'rails', 'application')
-
359
unless User.current.pref.warn_on_leaving_unsaved == '0'
-
359
tags << "\n".html_safe + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
-
end
-
359
tags
-
end
-
-
1
def favicon
-
466
"<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
-
end
-
-
1
def robot_exclusion_tag
-
2
'<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
-
end
-
-
# Returns true if arg is expected in the API response
-
1
def include_in_api_response?(arg)
-
unless @included_in_api_response
-
param = params[:include]
-
@included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
-
@included_in_api_response.collect!(&:strip)
-
end
-
@included_in_api_response.include?(arg.to_s)
-
end
-
-
# Returns options or nil if nometa param or X-Redmine-Nometa header
-
# was set in the request
-
1
def api_meta(options)
-
if params[:nometa].present? || request.headers['X-Redmine-Nometa']
-
# compatibility mode for activeresource clients that raise
-
# an error when unserializing an array with attributes
-
nil
-
else
-
options
-
end
-
end
-
-
1
private
-
-
1
def wiki_helper
-
4
helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
-
4
extend helper
-
4
return self
-
end
-
-
1
def link_to_content_update(text, url_params = {}, html_options = {})
-
108
link_to(text, url_params, html_options)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AttachmentsHelper
-
# Displays view/delete links to the attachments of the given object
-
# Options:
-
# :author -- author names are not displayed if set to false
-
1
def link_to_attachments(container, options = {})
-
options.assert_valid_keys(:author)
-
-
if container.attachments.any?
-
options = {:deletable => container.attachments_deletable?, :author => true}.merge(options)
-
render :partial => 'attachments/links', :locals => {:attachments => container.attachments, :options => options}
-
end
-
end
-
-
1
def render_api_attachment(attachment, api)
-
api.attachment do
-
api.id attachment.id
-
api.filename attachment.filename
-
api.filesize attachment.filesize
-
api.content_type attachment.content_type
-
api.description attachment.description
-
api.content_url url_for(:controller => 'attachments', :action => 'download', :id => attachment, :filename => attachment.filename, :only_path => false)
-
api.author(:id => attachment.author.id, :name => attachment.author.name) if attachment.author
-
api.created_on attachment.created_on
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module AuthSourcesHelper
-
1
def auth_source_partial_name(auth_source)
-
"form_#{auth_source.class.name.underscore}"
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module BoardsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module CalendarsHelper
-
1
def link_to_previous_month(year, month, options={})
-
target_year, target_month = if month == 1
-
[year - 1, 12]
-
else
-
[year, month - 1]
-
end
-
-
name = if target_month == 12
-
"#{month_name(target_month)} #{target_year}"
-
else
-
"#{month_name(target_month)}"
-
end
-
-
# \xc2\xab(utf-8) = «
-
link_to_month(("\xc2\xab " + name), target_year, target_month, options)
-
end
-
-
1
def link_to_next_month(year, month, options={})
-
target_year, target_month = if month == 12
-
[year + 1, 1]
-
else
-
[year, month + 1]
-
end
-
-
name = if target_month == 1
-
"#{month_name(target_month)} #{target_year}"
-
else
-
"#{month_name(target_month)}"
-
end
-
-
# \xc2\xbb(utf-8) = »
-
link_to_month((name + " \xc2\xbb"), target_year, target_month, options)
-
end
-
-
1
def link_to_month(link_name, year, month, options={})
-
link_to_content_update(h(link_name), params.merge(:year => year, :month => month))
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ContextMenusHelper
-
1
def context_menu_link(name, url, options={})
-
options[:class] ||= ''
-
if options.delete(:selected)
-
options[:class] << ' icon-checked disabled'
-
options[:disabled] = true
-
end
-
if options.delete(:disabled)
-
options.delete(:method)
-
options.delete(:confirm)
-
options.delete(:onclick)
-
options[:class] << ' disabled'
-
url = '#'
-
end
-
link_to h(name), url, options
-
end
-
-
1
def bulk_update_custom_field_context_menu_link(field, text, value)
-
context_menu_link h(text),
-
{:controller => 'issues', :action => 'bulk_update', :ids => @issues.collect(&:id), :issue => {'custom_field_values' => {field.id => value}}, :back_url => @back},
-
:method => :post,
-
:selected => (@issue && @issue.custom_field_value(field) == value)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module CustomFieldsHelper
-
-
1
def custom_fields_tabs
-
tabs = [{:name => 'IssueCustomField', :partial => 'custom_fields/index', :label => :label_issue_plural},
-
{:name => 'TimeEntryCustomField', :partial => 'custom_fields/index', :label => :label_spent_time},
-
{:name => 'ProjectCustomField', :partial => 'custom_fields/index', :label => :label_project_plural},
-
{:name => 'VersionCustomField', :partial => 'custom_fields/index', :label => :label_version_plural},
-
{:name => 'UserCustomField', :partial => 'custom_fields/index', :label => :label_user_plural},
-
{:name => 'GroupCustomField', :partial => 'custom_fields/index', :label => :label_group_plural},
-
{:name => 'TimeEntryActivityCustomField', :partial => 'custom_fields/index', :label => TimeEntryActivity::OptionName},
-
{:name => 'IssuePriorityCustomField', :partial => 'custom_fields/index', :label => IssuePriority::OptionName},
-
{:name => 'DocumentCategoryCustomField', :partial => 'custom_fields/index', :label => DocumentCategory::OptionName}
-
]
-
end
-
-
# Return custom field html tag corresponding to its format
-
1
def custom_field_tag(name, custom_value)
-
4
custom_field = custom_value.custom_field
-
4
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
-
4
field_name << "[]" if custom_field.multiple?
-
4
field_id = "#{name}_custom_field_values_#{custom_field.id}"
-
-
4
tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
-
-
4
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
-
4
case field_format.try(:edit_as)
-
when "date"
-
text_field_tag(field_name, custom_value.value, tag_options.merge(:size => 10)) +
-
calendar_for(field_id)
-
when "text"
-
text_area_tag(field_name, custom_value.value, tag_options.merge(:rows => 3))
-
when "bool"
-
4
hidden_field_tag(field_name, '0') + check_box_tag(field_name, '1', custom_value.true?, tag_options)
-
when "list"
-
blank_option = ''.html_safe
-
unless custom_field.multiple?
-
if custom_field.is_required?
-
unless custom_field.default_value.present?
-
blank_option = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---", :value => '')
-
end
-
else
-
blank_option = content_tag('option')
-
end
-
end
-
s = select_tag(field_name, blank_option + options_for_select(custom_field.possible_values_options(custom_value.customized), custom_value.value),
-
tag_options.merge(:multiple => custom_field.multiple?))
-
if custom_field.multiple?
-
s << hidden_field_tag(field_name, '')
-
end
-
s
-
else
-
text_field_tag(field_name, custom_value.value, tag_options)
-
end
-
end
-
-
# Return custom field label tag
-
1
def custom_field_label_tag(name, custom_value)
-
4
content_tag "label", h(custom_value.custom_field.name) +
-
4
(custom_value.custom_field.is_required? ? " <span class=\"required\">*</span>".html_safe : ""),
-
:for => "#{name}_custom_field_values_#{custom_value.custom_field.id}"
-
end
-
-
# Return custom field tag with its label tag
-
1
def custom_field_tag_with_label(name, custom_value)
-
4
custom_field_label_tag(name, custom_value) + custom_field_tag(name, custom_value)
-
end
-
-
1
def custom_field_tag_for_bulk_edit(name, custom_field, projects=nil)
-
field_name = "#{name}[custom_field_values][#{custom_field.id}]"
-
field_name << "[]" if custom_field.multiple?
-
field_id = "#{name}_custom_field_values_#{custom_field.id}"
-
-
tag_options = {:id => field_id, :class => "#{custom_field.field_format}_cf"}
-
-
field_format = Redmine::CustomFieldFormat.find_by_name(custom_field.field_format)
-
case field_format.try(:edit_as)
-
when "date"
-
text_field_tag(field_name, '', tag_options.merge(:size => 10)) +
-
calendar_for(field_id)
-
when "text"
-
text_area_tag(field_name, '', tag_options.merge(:rows => 3))
-
when "bool"
-
select_tag(field_name, options_for_select([[l(:label_no_change_option), ''],
-
[l(:general_text_yes), '1'],
-
[l(:general_text_no), '0']]), tag_options)
-
when "list"
-
options = []
-
options << [l(:label_no_change_option), ''] unless custom_field.multiple?
-
options << [l(:label_none), '__none__'] unless custom_field.is_required?
-
options += custom_field.possible_values_options(projects)
-
select_tag(field_name, options_for_select(options), tag_options.merge(:multiple => custom_field.multiple?))
-
else
-
text_field_tag(field_name, '', tag_options)
-
end
-
end
-
-
# Return a string used to display a custom value
-
1
def show_value(custom_value)
-
50
return "" unless custom_value
-
50
format_value(custom_value.value, custom_value.custom_field.field_format)
-
end
-
-
# Return a string used to display a custom value
-
1
def format_value(value, field_format)
-
50
if value.is_a?(Array)
-
value.collect {|v| format_value(v, field_format)}.compact.sort.join(', ')
-
else
-
50
Redmine::CustomFieldFormat.format_value(value, field_format)
-
end
-
end
-
-
# Return an array of custom field formats which can be used in select_tag
-
1
def custom_field_formats_for_select(custom_field)
-
Redmine::CustomFieldFormat.as_select(custom_field.class.customized_class.name)
-
end
-
-
# Renders the custom_values in api views
-
1
def render_api_custom_values(custom_values, api)
-
api.array :custom_fields do
-
custom_values.each do |custom_value|
-
attrs = {:id => custom_value.custom_field_id, :name => custom_value.custom_field.name}
-
attrs.merge!(:multiple => true) if custom_value.custom_field.multiple?
-
api.custom_field attrs do
-
if custom_value.value.is_a?(Array)
-
api.array :value do
-
custom_value.value.each do |value|
-
api.value value unless value.blank?
-
end
-
end
-
else
-
api.value custom_value.value
-
end
-
end
-
end
-
end unless custom_values.empty?
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module DocumentsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module EnumerationsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module GanttHelper
-
-
1
def gantt_zoom_link(gantt, in_or_out)
-
case in_or_out
-
when :in
-
if gantt.zoom < 4
-
link_to_content_update l(:text_zoom_in),
-
params.merge(gantt.params.merge(:zoom => (gantt.zoom+1))),
-
:class => 'icon icon-zoom-in'
-
else
-
content_tag('span', l(:text_zoom_in), :class => 'icon icon-zoom-in').html_safe
-
end
-
-
when :out
-
if gantt.zoom > 1
-
link_to_content_update l(:text_zoom_out),
-
params.merge(gantt.params.merge(:zoom => (gantt.zoom-1))),
-
:class => 'icon icon-zoom-out'
-
else
-
content_tag('span', l(:text_zoom_out), :class => 'icon icon-zoom-out').html_safe
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module GroupsHelper
-
# Options for the new membership projects combo-box
-
1
def options_for_membership_project_select(user, projects)
-
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
-
options << project_tree_options_for_select(projects) do |p|
-
{:disabled => (user.projects.include?(p))}
-
end
-
options
-
end
-
-
1
def group_settings_tabs
-
tabs = [{:name => 'general', :partial => 'groups/general', :label => :label_general},
-
{:name => 'users', :partial => 'groups/users', :label => :label_user_plural},
-
{:name => 'memberships', :partial => 'groups/memberships', :label => :label_project_plural}
-
]
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssueCategoriesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssueRelationsHelper
-
1
def collection_for_relation_type_select
-
2
values = IssueRelation::TYPES
-
36
values.keys.sort{|x,y| values[x][:order] <=> values[y][:order]}.collect{|k| [l(values[k][:name]), k]}
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssueStatusesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module IssuesHelper
-
1
include ApplicationHelper
-
-
1
def issue_list(issues, &block)
-
13
ancestors = []
-
13
issues.each do |issue|
-
84
while (ancestors.any? && !issue.is_descendant_of?(ancestors.last))
-
2
ancestors.pop
-
end
-
84
yield issue, ancestors.size
-
84
ancestors << issue unless issue.leaf?
-
end
-
end
-
-
# Renders a HTML/CSS tooltip
-
#
-
# To use, a trigger div is needed. This is a div with the class of "tooltip"
-
# that contains this method wrapped in a span with the class of "tip"
-
#
-
# <div class="tooltip"><%= link_to_issue(issue) %>
-
# <span class="tip"><%= render_issue_tooltip(issue) %></span>
-
# </div>
-
#
-
1
def render_issue_tooltip(issue)
-
@cached_label_status ||= l(:field_status)
-
@cached_label_start_date ||= l(:field_start_date)
-
@cached_label_due_date ||= l(:field_due_date)
-
@cached_label_assigned_to ||= l(:field_assigned_to)
-
@cached_label_priority ||= l(:field_priority)
-
@cached_label_project ||= l(:field_project)
-
-
link_to_issue(issue) + "<br /><br />".html_safe +
-
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />".html_safe +
-
"<strong>#{@cached_label_status}</strong>: #{h(issue.status.name)}<br />".html_safe +
-
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />".html_safe +
-
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />".html_safe +
-
"<strong>#{@cached_label_assigned_to}</strong>: #{h(issue.assigned_to)}<br />".html_safe +
-
"<strong>#{@cached_label_priority}</strong>: #{h(issue.priority.name)}".html_safe
-
end
-
-
1
def issue_heading(issue)
-
2
h("#{issue.tracker} ##{issue.id}")
-
end
-
-
1
def render_issue_subject_with_tree(issue)
-
2
s = ''
-
2
ancestors = issue.root? ? [] : issue.ancestors.visible.all
-
2
ancestors.each do |ancestor|
-
s << '<div>' + content_tag('p', link_to_issue(ancestor))
-
end
-
2
s << '<div>'
-
2
subject = h(issue.subject)
-
2
if issue.is_private?
-
subject = content_tag('span', l(:field_is_private), :class => 'private') + ' ' + subject
-
end
-
2
s << content_tag('h3', subject)
-
2
s << '</div>' * (ancestors.size + 1)
-
2
s.html_safe
-
end
-
-
1
def render_descendants_tree(issue)
-
1
s = '<form><table class="list issues">'
-
1
issue_list(issue.descendants.visible.sort_by(&:lft)) do |child, level|
-
s << content_tag('tr',
-
content_tag('td', check_box_tag("ids[]", child.id, false, :id => nil), :class => 'checkbox') +
-
content_tag('td', link_to_issue(child, :truncate => 60), :class => 'subject') +
-
content_tag('td', h(child.status)) +
-
content_tag('td', link_to_user(child.assigned_to)) +
-
content_tag('td', progress_bar(child.done_ratio, :width => '80px')),
-
2
:class => "issue issue-#{child.id} hascontextmenu #{level > 0 ? "idnt idnt-#{level}" : nil}")
-
end
-
1
s << '</table></form>'
-
1
s.html_safe
-
end
-
-
1
def render_custom_fields_rows(issue)
-
2
return if issue.custom_field_values.empty?
-
ordered_values = []
-
half = (issue.custom_field_values.size / 2.0).ceil
-
half.times do |i|
-
ordered_values << issue.custom_field_values[i]
-
ordered_values << issue.custom_field_values[i + half]
-
end
-
s = "<tr>\n"
-
n = 0
-
ordered_values.compact.each do |value|
-
s << "</tr>\n<tr>\n" if n > 0 && (n % 2) == 0
-
s << "\t<th>#{ h(value.custom_field.name) }:</th><td>#{ simple_format_without_paragraph(h(show_value(value))) }</td>\n"
-
n += 1
-
end
-
s << "</tr>\n"
-
s.html_safe
-
end
-
-
1
def issues_destroy_confirmation_message(issues)
-
4
issues = [issues] unless issues.is_a?(Array)
-
4
message = l(:text_issues_destroy_confirmation)
-
8
descendant_count = issues.inject(0) {|memo, i| memo += (i.right - i.left - 1)/2}
-
4
if descendant_count > 0
-
2
issues.each do |issue|
-
2
next if issue.root?
-
issues.each do |other_issue|
-
descendant_count -= 1 if issue.is_descendant_of?(other_issue)
-
end
-
end
-
2
if descendant_count > 0
-
2
message << "\n" + l(:text_issues_destroy_descendants_confirmation, :count => descendant_count)
-
end
-
end
-
4
message
-
end
-
-
1
def sidebar_queries
-
32
unless @sidebar_queries
-
16
@sidebar_queries = Query.visible.all(
-
:order => "#{Query.table_name}.name ASC",
-
# Project specific queries and global queries
-
16
:conditions => (@project.nil? ? ["project_id IS NULL"] : ["project_id IS NULL OR project_id = ?", @project.id])
-
)
-
end
-
32
@sidebar_queries
-
end
-
-
1
def query_links(title, queries)
-
# links to #index on issues/show
-
16
url_params = controller_name == 'issues' ? {:controller => 'issues', :action => 'index', :project_id => @project} : params
-
-
content_tag('h3', h(title)) +
-
queries.collect {|query|
-
80
css = 'query'
-
80
css << ' selected' if query == @query
-
80
link_to(h(query.name), url_params.merge(:query_id => query), :class => css)
-
16
}.join('<br />').html_safe
-
end
-
-
1
def render_sidebar_queries
-
16
out = ''.html_safe
-
96
queries = sidebar_queries.select {|q| !q.is_public?}
-
16
out << query_links(l(:label_my_queries), queries) if queries.any?
-
96
queries = sidebar_queries.select {|q| q.is_public?}
-
16
out << query_links(l(:label_query_plural), queries) if queries.any?
-
16
out
-
end
-
-
# Returns the textual representation of a journal details
-
# as an array of strings
-
1
def details_to_strings(details, no_html=false, options={})
-
232
options[:only_path] = (options[:only_path] == false ? false : true)
-
232
strings = []
-
232
values_by_field = {}
-
232
details.each do |detail|
-
322
if detail.property == 'cf'
-
field_id = detail.prop_key
-
field = CustomField.find_by_id(field_id)
-
if field && field.multiple?
-
values_by_field[field_id] ||= {:added => [], :deleted => []}
-
if detail.old_value
-
values_by_field[field_id][:deleted] << detail.old_value
-
end
-
if detail.value
-
values_by_field[field_id][:added] << detail.value
-
end
-
next
-
end
-
end
-
322
strings << show_detail(detail, no_html, options)
-
end
-
232
values_by_field.each do |field_id, changes|
-
detail = JournalDetail.new(:property => 'cf', :prop_key => field_id)
-
if changes[:added].any?
-
detail.value = changes[:added]
-
strings << show_detail(detail, no_html, options)
-
elsif changes[:deleted].any?
-
detail.old_value = changes[:deleted]
-
strings << show_detail(detail, no_html, options)
-
end
-
end
-
232
strings
-
end
-
-
# Returns the textual representation of a single journal detail
-
1
def show_detail(detail, no_html=false, options={})
-
322
multiple = false
-
322
case detail.property
-
when 'attr'
-
322
field = detail.prop_key.to_s.gsub(/\_id$/, "")
-
322
label = l(("field_" + field).to_sym)
-
322
case detail.prop_key
-
when 'due_date', 'start_date'
-
24
value = format_date(detail.value.to_date) if detail.value
-
24
old_value = format_date(detail.old_value.to_date) if detail.old_value
-
-
when 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
-
'priority_id', 'category_id', 'fixed_version_id'
-
138
value = find_name_by_reflection(field, detail.value)
-
138
old_value = find_name_by_reflection(field, detail.old_value)
-
-
when 'estimated_hours'
-
2
value = "%0.02f" % detail.value.to_f unless detail.value.blank?
-
2
old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
-
-
when 'parent_id'
-
label = l(:field_parent_issue)
-
value = "##{detail.value}" unless detail.value.blank?
-
old_value = "##{detail.old_value}" unless detail.old_value.blank?
-
-
when 'is_private'
-
value = l(detail.value == "0" ? :general_text_No : :general_text_Yes) unless detail.value.blank?
-
old_value = l(detail.old_value == "0" ? :general_text_No : :general_text_Yes) unless detail.old_value.blank?
-
end
-
when 'cf'
-
custom_field = CustomField.find_by_id(detail.prop_key)
-
if custom_field
-
multiple = custom_field.multiple?
-
label = custom_field.name
-
value = format_value(detail.value, custom_field.field_format) if detail.value
-
old_value = format_value(detail.old_value, custom_field.field_format) if detail.old_value
-
end
-
when 'attachment'
-
label = l(:label_attachment)
-
end
-
322
call_hook(:helper_issues_show_detail_after_setting,
-
{:detail => detail, :label => label, :value => value, :old_value => old_value })
-
-
322
label ||= detail.prop_key
-
322
value ||= detail.value
-
322
old_value ||= detail.old_value
-
-
322
unless no_html
-
161
label = content_tag('strong', label)
-
161
old_value = content_tag("i", h(old_value)) if detail.old_value
-
161
old_value = content_tag("strike", old_value) if detail.old_value and detail.value.blank?
-
161
if detail.property == 'attachment' && !value.blank? && atta = Attachment.find_by_id(detail.prop_key)
-
# Link to the attachment if it has not been removed
-
value = link_to_attachment(atta, :download => true, :only_path => options[:only_path])
-
if options[:only_path] != false && atta.is_text?
-
value += link_to(
-
image_tag('magnifier.png'),
-
:controller => 'attachments', :action => 'show',
-
:id => atta, :filename => atta.filename
-
)
-
end
-
else
-
161
value = content_tag("i", h(value)) if value
-
end
-
end
-
-
322
if detail.property == 'attr' && detail.prop_key == 'description'
-
s = l(:text_journal_changed_no_detail, :label => label)
-
unless no_html
-
diff_link = link_to 'diff',
-
{:controller => 'journals', :action => 'diff', :id => detail.journal_id,
-
:detail_id => detail.id, :only_path => options[:only_path]},
-
:title => l(:label_view_diff)
-
s << " (#{ diff_link })"
-
end
-
s.html_safe
-
322
elsif detail.value.present?
-
322
case detail.property
-
when 'attr', 'cf'
-
322
if detail.old_value.present?
-
266
l(:text_journal_changed, :label => label, :old => old_value, :new => value).html_safe
-
56
elsif multiple
-
l(:text_journal_added, :label => label, :value => value).html_safe
-
else
-
56
l(:text_journal_set_to, :label => label, :value => value).html_safe
-
end
-
when 'attachment'
-
l(:text_journal_added, :label => label, :value => value).html_safe
-
end
-
else
-
l(:text_journal_deleted, :label => label, :old => old_value).html_safe
-
end
-
end
-
-
# Find the name of an associated record stored in the field attribute
-
1
def find_name_by_reflection(field, id)
-
276
association = Issue.reflect_on_association(field.to_sym)
-
276
if association
-
276
record = association.class_name.constantize.find_by_id(id)
-
276
return record.name if record
-
end
-
end
-
-
# Renders issue children recursively
-
1
def render_api_issue_children(issue, api)
-
return if issue.leaf?
-
api.array :children do
-
issue.children.each do |child|
-
api.issue(:id => child.id) do
-
api.tracker(:id => child.tracker_id, :name => child.tracker.name) unless child.tracker.nil?
-
api.subject child.subject
-
render_api_issue_children(child, api)
-
end
-
end
-
end
-
end
-
-
1
def issues_to_csv(issues, project, query, options={})
-
decimal_separator = l(:general_csv_decimal_separator)
-
encoding = l(:general_csv_encoding)
-
columns = (options[:columns] == 'all' ? query.available_columns : query.columns)
-
-
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
-
# csv header fields
-
csv << [ "#" ] + columns.collect {|c| Redmine::CodesetUtil.from_utf8(c.caption.to_s, encoding) } +
-
(options[:description] ? [Redmine::CodesetUtil.from_utf8(l(:field_description), encoding)] : [])
-
-
# csv lines
-
issues.each do |issue|
-
col_values = columns.collect do |column|
-
s = if column.is_a?(QueryCustomFieldColumn)
-
cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
-
show_value(cv)
-
else
-
value = column.value(issue)
-
if value.is_a?(Date)
-
format_date(value)
-
elsif value.is_a?(Time)
-
format_time(value)
-
elsif value.is_a?(Float)
-
("%.2f" % value).gsub('.', decimal_separator)
-
else
-
value
-
end
-
end
-
s.to_s
-
end
-
csv << [ issue.id.to_s ] + col_values.collect {|c| Redmine::CodesetUtil.from_utf8(c.to_s, encoding) } +
-
(options[:description] ? [Redmine::CodesetUtil.from_utf8(issue.description, encoding)] : [])
-
end
-
end
-
export
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module JournalsHelper
-
1
def render_notes(issue, journal, options={})
-
content = ''
-
editable = User.current.logged? && (User.current.allowed_to?(:edit_issue_notes, issue.project) || (journal.user == User.current && User.current.allowed_to?(:edit_own_issue_notes, issue.project)))
-
links = []
-
if !journal.notes.blank?
-
links << link_to_remote(image_tag('comment.png'),
-
{ :url => {:controller => 'journals', :action => 'new', :id => issue, :journal_id => journal} },
-
:title => l(:button_quote)) if options[:reply_links]
-
links << link_to_in_place_notes_editor(image_tag('edit.png'), "journal-#{journal.id}-notes",
-
{ :controller => 'journals', :action => 'edit', :id => journal },
-
:title => l(:button_edit)) if editable
-
end
-
content << content_tag('div', links.join(' ').html_safe, :class => 'contextual') unless links.empty?
-
content << textilizable(journal, :notes)
-
css_classes = "wiki"
-
css_classes << " editable" if editable
-
content_tag('div', content.html_safe, :id => "journal-#{journal.id}-notes", :class => css_classes)
-
end
-
-
1
def link_to_in_place_notes_editor(text, field_id, url, options={})
-
onclick = "new Ajax.Request('#{url_for(url)}', {asynchronous:true, evalScripts:true, method:'get'}); return false;"
-
link_to text, '#', options.merge(:onclick => onclick)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MailHandlerHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MembersHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MessagesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module MyHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module NewsHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ProjectsHelper
-
1
def link_to_version(version, options = {})
-
10
return '' unless version && version.is_a?(Version)
-
10
link_to_if version.visible?, format_version_name(version), { :controller => 'versions', :action => 'show', :id => version }, options
-
end
-
-
1
def project_settings_tabs
-
tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
-
{:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
-
{:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
-
{:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
-
{:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural},
-
{:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
-
{:name => 'repositories', :action => :manage_repository, :partial => 'projects/settings/repositories', :label => :label_repository_plural},
-
{:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
-
{:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
-
]
-
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
-
end
-
-
1
def parent_project_select_tag(project)
-
selected = project.parent
-
# retrieve the requested parent project
-
parent_id = (params[:project] && params[:project][:parent_id]) || params[:parent_id]
-
if parent_id
-
selected = (parent_id.blank? ? nil : Project.find(parent_id))
-
end
-
-
options = ''
-
options << "<option value=''></option>" if project.allowed_parents.include?(nil)
-
options << project_tree_options_for_select(project.allowed_parents.compact, :selected => selected)
-
content_tag('select', options.html_safe, :name => 'project[parent_id]', :id => 'project_parent_id')
-
end
-
-
# Renders a tree of projects as a nested set of unordered lists
-
# The given collection may be a subset of the whole project tree
-
# (eg. some intermediate nodes are private and can not be seen)
-
1
def render_project_hierarchy(projects)
-
s = ''
-
if projects.any?
-
ancestors = []
-
original_project = @project
-
projects.each do |project|
-
# set the project environment to please macros.
-
@project = project
-
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
-
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
-
else
-
ancestors.pop
-
s << "</li>"
-
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
-
ancestors.pop
-
s << "</ul></li>\n"
-
end
-
end
-
classes = (ancestors.empty? ? 'root' : 'child')
-
s << "<li class='#{classes}'><div class='#{classes}'>" +
-
link_to_project(project, {}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
-
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
-
s << "</div>\n"
-
ancestors << project
-
end
-
s << ("</li></ul>\n" * ancestors.size)
-
@project = original_project
-
end
-
s.html_safe
-
end
-
-
# Returns a set of options for a select field, grouped by project.
-
1
def version_options_for_select(versions, selected=nil)
-
20
grouped = Hash.new {|h,k| h[k] = []}
-
4
versions.each do |version|
-
28
grouped[version.project.name] << [version.name, version.id]
-
end
-
# Add in the selected
-
4
if selected && !versions.include?(selected)
-
grouped[selected.project.name] << [selected.name, selected.id]
-
end
-
-
4
if grouped.keys.size > 1
-
4
grouped_options_for_select(grouped, selected && selected.id)
-
else
-
options_for_select((grouped.values.first || []), selected && selected.id)
-
end
-
end
-
-
1
def format_version_sharing(sharing)
-
sharing = 'none' unless Version::VERSION_SHARINGS.include?(sharing)
-
l("label_version_sharing_#{sharing}")
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module QueriesHelper
-
-
1
def operators_for_select(filter_type)
-
2100
Query.operators_by_filter_type[filter_type].collect {|o| [l(Query.operators[o]), o]}
-
end
-
-
1
def column_header(column)
-
78
column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
-
:default_order => column.default_order) :
-
content_tag('th', h(column.caption))
-
end
-
-
1
def column_content(column, issue)
-
508
value = column.value(issue)
-
508
if value.is_a?(Array)
-
value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ').html_safe
-
else
-
508
column_value(column, issue, value)
-
end
-
end
-
-
1
def column_value(column, issue, value)
-
508
case value.class.name
-
when 'String'
-
82
if column.name == :subject
-
82
link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
-
else
-
h(value)
-
end
-
when 'Time'
-
82
format_time(value)
-
when 'Date'
-
format_date(value)
-
when 'Fixnum', 'Float'
-
16
if column.name == :done_ratio
-
progress_bar(value, :width => '80px')
-
16
elsif column.name == :spent_hours
-
sprintf "%.2f", value
-
else
-
16
h(value.to_s)
-
end
-
when 'User'
-
10
link_to_user value
-
when 'Project'
-
link_to_project value
-
when 'Version'
-
link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
-
when 'TrueClass'
-
l(:general_text_Yes)
-
when 'FalseClass'
-
l(:general_text_No)
-
when 'Issue'
-
link_to_issue(value, :subject => false)
-
else
-
318
h(value)
-
end
-
end
-
-
# Retrieve query from session or build a new query
-
1
def retrieve_query
-
14
if !params[:query_id].blank?
-
cond = "project_id IS NULL"
-
cond << " OR project_id = #{@project.id}" if @project
-
@query = Query.find(params[:query_id], :conditions => cond)
-
raise ::Unauthorized unless @query.visible?
-
@query.project = @project
-
session[:query] = {:id => @query.id, :project_id => @query.project_id}
-
sort_clear
-
14
elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
-
# Give it a name, required to be valid
-
5
@query = Query.new(:name => "_")
-
5
@query.project = @project
-
5
build_query_from_params
-
5
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
-
else
-
# retrieve from session
-
9
@query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
-
9
@query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
-
9
@query.project = @project
-
end
-
end
-
-
1
def retrieve_query_from_session
-
2
if session[:query]
-
if session[:query][:id]
-
@query = Query.find_by_id(session[:query][:id])
-
return unless @query
-
else
-
@query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
-
end
-
if session[:query].has_key?(:project_id)
-
@query.project_id = session[:query][:project_id]
-
else
-
@query.project = @project
-
end
-
@query
-
end
-
end
-
-
1
def build_query_from_params
-
5
if params[:fields] || params[:f]
-
@query.filters = {}
-
@query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
-
else
-
5
@query.available_filters.keys.each do |field|
-
110
@query.add_short_filter(field, params[field]) if params[field]
-
end
-
end
-
5
@query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
-
5
@query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ReportsHelper
-
-
1
def aggregate(data, criteria)
-
a = 0
-
data.each { |row|
-
match = 1
-
criteria.each { |k, v|
-
match = 0 unless (row[k].to_s == v.to_s) || (k == 'closed' && row[k] == (v == 0 ? "f" : "t"))
-
} unless criteria.nil?
-
a = a + row["total"].to_i if match == 1
-
} unless data.nil?
-
a
-
end
-
-
1
def aggregate_link(data, criteria, *args)
-
a = aggregate data, criteria
-
a > 0 ? link_to(h(a), *args) : '-'
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
1
require 'redmine/codeset_util'
-
-
1
module RepositoriesHelper
-
1
def format_revision(revision)
-
if revision.respond_to? :format_identifier
-
revision.format_identifier
-
else
-
revision.to_s
-
end
-
end
-
-
1
def truncate_at_line_break(text, length = 255)
-
if text
-
text.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...')
-
end
-
end
-
-
1
def render_properties(properties)
-
unless properties.nil? || properties.empty?
-
content = ''
-
properties.keys.sort.each do |property|
-
content << content_tag('li', "<b>#{h property}</b>: <span>#{h properties[property]}</span>".html_safe)
-
end
-
content_tag('ul', content.html_safe, :class => 'properties')
-
end
-
end
-
-
1
def render_changeset_changes
-
changes = @changeset.filechanges.find(:all, :limit => 1000, :order => 'path').collect do |change|
-
case change.action
-
when 'A'
-
# Detects moved/copied files
-
if !change.from_path.blank?
-
change.action =
-
@changeset.filechanges.detect {|c| c.action == 'D' && c.path == change.from_path} ? 'R' : 'C'
-
end
-
change
-
when 'D'
-
@changeset.filechanges.detect {|c| c.from_path == change.path} ? nil : change
-
else
-
change
-
end
-
end.compact
-
-
tree = { }
-
changes.each do |change|
-
p = tree
-
dirs = change.path.to_s.split('/').select {|d| !d.blank?}
-
path = ''
-
dirs.each do |dir|
-
path += '/' + dir
-
p[:s] ||= {}
-
p = p[:s]
-
p[path] ||= {}
-
p = p[path]
-
end
-
p[:c] = change
-
end
-
render_changes_tree(tree[:s])
-
end
-
-
1
def render_changes_tree(tree)
-
return '' if tree.nil?
-
output = ''
-
output << '<ul>'
-
tree.keys.sort.each do |file|
-
style = 'change'
-
text = File.basename(h(file))
-
if s = tree[file][:s]
-
style << ' folder'
-
path_param = to_path_param(@repository.relative_path(file))
-
text = link_to(h(text), :controller => 'repositories',
-
:action => 'show',
-
:id => @project,
-
:repository_id => @repository.identifier_param,
-
:path => path_param,
-
:rev => @changeset.identifier)
-
output << "<li class='#{style}'>#{text}"
-
output << render_changes_tree(s)
-
output << "</li>"
-
elsif c = tree[file][:c]
-
style << " change-#{c.action}"
-
path_param = to_path_param(@repository.relative_path(c.path))
-
text = link_to(h(text), :controller => 'repositories',
-
:action => 'entry',
-
:id => @project,
-
:repository_id => @repository.identifier_param,
-
:path => path_param,
-
:rev => @changeset.identifier) unless c.action == 'D'
-
text << " - #{h(c.revision)}" unless c.revision.blank?
-
text << ' ('.html_safe + link_to(l(:label_diff), :controller => 'repositories',
-
:action => 'diff',
-
:id => @project,
-
:repository_id => @repository.identifier_param,
-
:path => path_param,
-
:rev => @changeset.identifier) + ') '.html_safe if c.action == 'M'
-
text << ' '.html_safe + content_tag('span', h(c.from_path), :class => 'copied-from') unless c.from_path.blank?
-
output << "<li class='#{style}'>#{text}</li>"
-
end
-
end
-
output << '</ul>'
-
output.html_safe
-
end
-
-
1
def repository_field_tags(form, repository)
-
method = repository.class.name.demodulize.underscore + "_field_tags"
-
if repository.is_a?(Repository) &&
-
respond_to?(method) && method != 'repository_field_tags'
-
send(method, form, repository)
-
end
-
end
-
-
1
def scm_select_tag(repository)
-
scm_options = [["--- #{l(:actionview_instancetag_blank_option)} ---", '']]
-
Redmine::Scm::Base.all.each do |scm|
-
if Setting.enabled_scm.include?(scm) ||
-
(repository && repository.class.name.demodulize == scm)
-
scm_options << ["Repository::#{scm}".constantize.scm_name, scm]
-
end
-
end
-
select_tag('repository_scm',
-
options_for_select(scm_options, repository.class.name.demodulize),
-
:disabled => (repository && !repository.new_record?),
-
:onchange => remote_function(
-
:url => new_project_repository_path(@project),
-
:method => :get,
-
:update => 'content',
-
:with => "Form.serialize(this.form)")
-
)
-
end
-
-
1
def with_leading_slash(path)
-
path.to_s.starts_with?('/') ? path : "/#{path}"
-
end
-
-
1
def without_leading_slash(path)
-
path.gsub(%r{^/+}, '')
-
end
-
-
1
def subversion_field_tags(form, repository)
-
content_tag('p', form.text_field(:url, :size => 60, :required => true,
-
:disabled => (repository && !repository.root_url.blank?)) +
-
'<br />'.html_safe +
-
'(file:///, http://, https://, svn://, svn+[tunnelscheme]://)') +
-
content_tag('p', form.text_field(:login, :size => 30)) +
-
content_tag('p', form.password_field(
-
:password, :size => 30, :name => 'ignore',
-
:value => ((repository.new_record? || repository.password.blank?) ? '' : ('x'*15)),
-
:onfocus => "this.value=''; this.name='repository[password]';",
-
:onchange => "this.name='repository[password]';"))
-
end
-
-
1
def darcs_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => (repository && !repository.new_record?))) +
-
content_tag('p', form.select(
-
:log_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_commit_logs_encoding), :required => true))
-
end
-
-
1
def mercurial_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => (repository && !repository.root_url.blank?)
-
) +
-
'<br />'.html_safe + l(:text_mercurial_repository_note)) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note))
-
end
-
-
1
def git_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => (repository && !repository.root_url.blank?)
-
) +
-
'<br />'.html_safe +
-
l(:text_git_repository_note)) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note)) +
-
content_tag('p', form.check_box(
-
:extra_report_last_commit,
-
:label => l(:label_git_report_last_commit)
-
))
-
end
-
-
1
def cvs_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:root_url,
-
:label => l(:field_cvsroot),
-
:size => 60, :required => true,
-
:disabled => !repository.new_record?)) +
-
content_tag('p', form.text_field(
-
:url,
-
:label => l(:field_cvs_module),
-
:size => 30, :required => true,
-
:disabled => !repository.new_record?)) +
-
content_tag('p', form.select(
-
:log_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_commit_logs_encoding), :required => true)) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note))
-
end
-
-
1
def bazaar_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_path_to_repository),
-
:size => 60, :required => true,
-
:disabled => (repository && !repository.new_record?))) +
-
content_tag('p', form.select(
-
:log_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_commit_logs_encoding), :required => true))
-
end
-
-
1
def filesystem_field_tags(form, repository)
-
content_tag('p', form.text_field(
-
:url, :label => l(:field_root_directory),
-
:size => 60, :required => true,
-
:disabled => (repository && !repository.root_url.blank?))) +
-
content_tag('p', form.select(
-
:path_encoding, [nil] + Setting::ENCODINGS,
-
:label => l(:field_scm_path_encoding)
-
) +
-
'<br />'.html_safe + l(:text_scm_path_encoding_note))
-
end
-
-
1
def index_commits(commits, heads)
-
return nil if commits.nil? or commits.first.parents.nil?
-
-
refs_map = {}
-
heads.each do |head|
-
refs_map[head.scmid] ||= []
-
refs_map[head.scmid] << head
-
end
-
-
commits_by_scmid = {}
-
commits.reverse.each_with_index do |commit, commit_index|
-
-
commits_by_scmid[commit.scmid] = {
-
:parent_scmids => commit.parents.collect { |parent| parent.scmid },
-
:rdmid => commit_index,
-
:refs => refs_map.include?(commit.scmid) ? refs_map[commit.scmid].join(" ") : nil,
-
:scmid => commit.scmid,
-
:href => block_given? ? yield(commit.scmid) : commit.scmid
-
}
-
end
-
-
heads.sort! { |head1, head2| head1.to_s <=> head2.to_s }
-
-
space = nil
-
heads.each do |head|
-
if commits_by_scmid.include? head.scmid
-
space = index_head((space || -1) + 1, head, commits_by_scmid)
-
end
-
end
-
-
# when no head matched anything use first commit
-
space ||= index_head(0, commits.first, commits_by_scmid)
-
-
return commits_by_scmid, space
-
end
-
-
1
def index_head(space, commit, commits_by_scmid)
-
-
stack = [[space, commits_by_scmid[commit.scmid]]]
-
max_space = space
-
-
until stack.empty?
-
space, commit = stack.pop
-
commit[:space] = space if commit[:space].nil?
-
-
space -= 1
-
commit[:parent_scmids].each_with_index do |parent_scmid, parent_index|
-
-
parent_commit = commits_by_scmid[parent_scmid]
-
-
if parent_commit and parent_commit[:space].nil?
-
-
stack.unshift [space += 1, parent_commit]
-
end
-
end
-
max_space = space if max_space < space
-
end
-
max_space
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module RolesHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module SearchHelper
-
1
def highlight_tokens(text, tokens)
-
return text unless text && tokens && !tokens.empty?
-
re_tokens = tokens.collect {|t| Regexp.escape(t)}
-
regexp = Regexp.new "(#{re_tokens.join('|')})", Regexp::IGNORECASE
-
result = ''
-
text.split(regexp).each_with_index do |words, i|
-
if result.length > 1200
-
# maximum length of the preview reached
-
result << '...'
-
break
-
end
-
words = words.mb_chars
-
if i.even?
-
result << h(words.length > 100 ? "#{words.slice(0..44)} ... #{words.slice(-45..-1)}" : words)
-
else
-
t = (tokens.index(words.downcase) || 0) % 4
-
result << content_tag('span', h(words), :class => "highlight token-#{t}")
-
end
-
end
-
result.html_safe
-
end
-
-
1
def type_label(t)
-
l("label_#{t.singularize}_plural", :default => t.to_s.humanize)
-
end
-
-
1
def project_select_tag
-
options = [[l(:label_project_all), 'all']]
-
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty?
-
options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty?
-
options << [@project.name, ''] unless @project.nil?
-
label_tag("scope", l(:description_project_scope), :class => "hidden-for-sighted") +
-
select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
-
end
-
-
1
def render_results_by_type(results_by_type)
-
links = []
-
# Sorts types by results count
-
results_by_type.keys.sort {|a, b| results_by_type[b] <=> results_by_type[a]}.each do |t|
-
c = results_by_type[t]
-
next if c == 0
-
text = "#{type_label(t)} (#{c})"
-
links << link_to(h(text), :q => params[:q], :titles_only => params[:titles_only],
-
:all_words => params[:all_words], :scope => params[:scope], t => 1)
-
end
-
('<ul>'.html_safe +
-
links.map {|link| content_tag('li', link)}.join(' ').html_safe +
-
'</ul>'.html_safe) unless links.empty?
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module SettingsHelper
-
1
def administration_settings_tabs
-
tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
-
{:name => 'display', :partial => 'settings/display', :label => :label_display},
-
{:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
-
{:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
-
{:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
-
{:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
-
{:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
-
{:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
-
]
-
end
-
-
1
def setting_select(setting, choices, options={})
-
if blank_text = options.delete(:blank)
-
choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
-
end
-
setting_label(setting, options).html_safe +
-
select_tag("settings[#{setting}]",
-
options_for_select(choices, Setting.send(setting).to_s),
-
options).html_safe
-
end
-
-
1
def setting_multiselect(setting, choices, options={})
-
setting_values = Setting.send(setting)
-
setting_values = [] unless setting_values.is_a?(Array)
-
-
setting_label(setting, options).html_safe +
-
hidden_field_tag("settings[#{setting}][]", '').html_safe +
-
choices.collect do |choice|
-
text, value = (choice.is_a?(Array) ? choice : [choice, choice])
-
content_tag(
-
'label',
-
check_box_tag(
-
"settings[#{setting}][]",
-
value,
-
Setting.send(setting).include?(value)
-
) + text.to_s,
-
:class => 'block'
-
)
-
end.join.html_safe
-
end
-
-
1
def setting_text_field(setting, options={})
-
setting_label(setting, options).html_safe +
-
text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
-
end
-
-
1
def setting_text_area(setting, options={})
-
setting_label(setting, options).html_safe +
-
text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
-
end
-
-
1
def setting_check_box(setting, options={})
-
setting_label(setting, options).html_safe +
-
hidden_field_tag("settings[#{setting}]", 0).html_safe +
-
check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
-
end
-
-
1
def setting_label(setting, options={})
-
label = options.delete(:label)
-
label != false ? content_tag("label", l(label || "setting_#{setting}")).html_safe : ''
-
end
-
-
# Renders a notification field for a Redmine::Notifiable option
-
1
def notification_field(notifiable)
-
return content_tag(:label,
-
check_box_tag('settings[notified_events][]',
-
notifiable.name,
-
Setting.notified_events.include?(notifiable.name)).html_safe +
-
l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
-
:class => notifiable.parent.present? ? "parent" : '').html_safe
-
end
-
end
-
# encoding: utf-8
-
#
-
# Helpers to sort tables using clickable column headers.
-
#
-
# Author: Stuart Rackham <srackham@methods.co.nz>, March 2005.
-
# Jean-Philippe Lang, 2009
-
# License: This source code is released under the MIT license.
-
#
-
# - Consecutive clicks toggle the column's sort order.
-
# - Sort state is maintained by a session hash entry.
-
# - CSS classes identify sort column and state.
-
# - Typically used in conjunction with the Pagination module.
-
#
-
# Example code snippets:
-
#
-
# Controller:
-
#
-
# helper :sort
-
# include SortHelper
-
#
-
# def list
-
# sort_init 'last_name'
-
# sort_update %w(first_name last_name)
-
# @items = Contact.find_all nil, sort_clause
-
# end
-
#
-
# Controller (using Pagination module):
-
#
-
# helper :sort
-
# include SortHelper
-
#
-
# def list
-
# sort_init 'last_name'
-
# sort_update %w(first_name last_name)
-
# @contact_pages, @items = paginate :contacts,
-
# :order_by => sort_clause,
-
# :per_page => 10
-
# end
-
#
-
# View (table header in list.rhtml):
-
#
-
# <thead>
-
# <tr>
-
# <%= sort_header_tag('id', :title => 'Sort by contact ID') %>
-
# <%= sort_header_tag('last_name', :caption => 'Name') %>
-
# <%= sort_header_tag('phone') %>
-
# <%= sort_header_tag('address', :width => 200) %>
-
# </tr>
-
# </thead>
-
#
-
# - Introduces instance variables: @sort_default, @sort_criteria
-
# - Introduces param :sort
-
#
-
-
1
module SortHelper
-
1
class SortCriteria
-
-
1
def initialize
-
126
@criteria = []
-
end
-
-
1
def available_criteria=(criteria)
-
18
unless criteria.is_a?(Hash)
-
criteria = criteria.inject({}) {|h,k| h[k] = k; h}
-
end
-
18
@available_criteria = criteria
-
end
-
-
1
def from_param(param)
-
244
@criteria = param.to_s.split(',').collect {|s| s.split(':')[0..1]}
-
126
normalize!
-
end
-
-
1
def criteria=(arg)
-
8
@criteria = arg
-
8
normalize!
-
end
-
-
1
def to_param
-
561
@criteria.collect {|k,o| k + (o ? '' : ':desc')}.join(',')
-
end
-
-
1
def to_sql
-
18
sql = @criteria.collect do |k,o|
-
18
if s = @available_criteria[k]
-
28
(o ? s.to_a : s.to_a.collect {|c| append_desc(c)}).join(', ')
-
end
-
end.compact.join(', ')
-
18
sql.blank? ? nil : sql
-
end
-
-
1
def add!(key, asc)
-
216
@criteria.delete_if {|k,o| k == key}
-
108
@criteria = [[key, asc]] + @criteria
-
108
normalize!
-
end
-
-
1
def add(*args)
-
108
r = self.class.new.from_param(to_param)
-
108
r.add!(*args)
-
108
r
-
end
-
-
1
def first_key
-
108
@criteria.first && @criteria.first.first
-
end
-
-
1
def first_asc?
-
15
@criteria.first && @criteria.first.last
-
end
-
-
1
def empty?
-
18
@criteria.empty?
-
end
-
-
1
private
-
-
1
def normalize!
-
242
@criteria ||= []
-
569
@criteria = @criteria.collect {|s| s = s.to_a; [s.first, (s.last == false || s.last == 'desc') ? false : true]}
-
260
@criteria = @criteria.select {|k,o| @available_criteria.has_key?(k)} if @available_criteria
-
242
@criteria.slice!(3)
-
242
self
-
end
-
-
# Appends DESC to the sort criterion unless it has a fixed order
-
1
def append_desc(criterion)
-
10
if criterion =~ / (asc|desc)$/i
-
criterion
-
else
-
10
"#{criterion} DESC"
-
end
-
end
-
end
-
-
1
def sort_name
-
18
controller_name + '_' + action_name + '_sort'
-
end
-
-
# Initializes the default sort.
-
# Examples:
-
#
-
# sort_init 'name'
-
# sort_init 'id', 'desc'
-
# sort_init ['name', ['id', 'desc']]
-
# sort_init [['name', 'desc'], ['id', 'desc']]
-
#
-
1
def sort_init(*args)
-
18
case args.size
-
when 1
-
14
@sort_default = args.first.is_a?(Array) ? args.first : [[args.first]]
-
when 2
-
4
@sort_default = [[args.first, args.last]]
-
else
-
raise ArgumentError
-
end
-
end
-
-
# Updates the sort state. Call this in the controller prior to calling
-
# sort_clause.
-
# - criteria can be either an array or a hash of allowed keys
-
#
-
1
def sort_update(criteria, sort_name=nil)
-
18
sort_name ||= self.sort_name
-
18
@sort_criteria = SortCriteria.new
-
18
@sort_criteria.available_criteria = criteria
-
18
@sort_criteria.from_param(params[:sort] || session[sort_name])
-
18
@sort_criteria.criteria = @sort_default if @sort_criteria.empty?
-
18
session[sort_name] = @sort_criteria.to_param
-
end
-
-
# Clears the sort criteria session data
-
#
-
1
def sort_clear
-
session[sort_name] = nil
-
end
-
-
# Returns an SQL sort clause corresponding to the current sort state.
-
# Use this to sort the controller's table items collection.
-
#
-
1
def sort_clause()
-
18
@sort_criteria.to_sql
-
end
-
-
# Returns a link which sorts by the named column.
-
#
-
# - column is the name of an attribute in the sorted record collection.
-
# - the optional caption explicitly specifies the displayed link text.
-
# - 2 CSS classes reflect the state of the link: sort and asc or desc
-
#
-
1
def sort_link(column, caption, default_order)
-
108
css, order = nil, default_order
-
-
108
if column.to_s == @sort_criteria.first_key
-
15
if @sort_criteria.first_asc?
-
6
css = 'sort asc'
-
6
order = 'desc'
-
else
-
9
css = 'sort desc'
-
9
order = 'asc'
-
end
-
end
-
108
caption = column.to_s.humanize unless caption
-
-
108
sort_options = { :sort => @sort_criteria.add(column.to_s, order).to_param }
-
108
url_options = params.merge(sort_options)
-
-
# Add project_id to url_options
-
108
url_options = url_options.merge(:project_id => params[:project_id]) if params.has_key?(:project_id)
-
-
108
link_to_content_update(h(caption), url_options, :class => css)
-
end
-
-
# Returns a table header <th> tag with a sort link for the named column
-
# attribute.
-
#
-
# Options:
-
# :caption The displayed link name (defaults to titleized column name).
-
# :title The tag's 'title' attribute (defaults to 'Sort by :caption').
-
#
-
# Other options hash entries generate additional table header tag attributes.
-
#
-
# Example:
-
#
-
# <%= sort_header_tag('id', :title => 'Sort by contact ID', :width => 40) %>
-
#
-
1
def sort_header_tag(column, options = {})
-
108
caption = options.delete(:caption) || column.to_s.humanize
-
108
default_order = options.delete(:default_order) || 'asc'
-
108
options[:title] = l(:label_sort_by, "\"#{caption}\"") unless options[:title]
-
108
content_tag('th', sort_link(column, caption, default_order), options)
-
end
-
end
-
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module TimelogHelper
-
1
include ApplicationHelper
-
-
1
def render_timelog_breadcrumb
-
4
links = []
-
4
links << link_to(l(:label_project_all), {:project_id => nil, :issue_id => nil})
-
4
links << link_to(h(@project), {:project_id => @project, :issue_id => nil}) if @project
-
4
if @issue
-
2
if @issue.visible?
-
2
links << link_to_issue(@issue, :subject => false)
-
else
-
links << "##{@issue.id}"
-
end
-
end
-
4
breadcrumb links
-
end
-
-
# Returns a collection of activities for a select field. time_entry
-
# is optional and will be used to check if the selected TimeEntryActivity
-
# is active.
-
1
def activity_collection_for_select_options(time_entry=nil, project=nil)
-
4
project ||= @project
-
4
if project.nil?
-
activities = TimeEntryActivity.shared.active
-
else
-
4
activities = project.activities
-
end
-
-
4
collection = []
-
4
if time_entry && time_entry.activity && !time_entry.activity.active?
-
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ]
-
else
-
4
collection << [ "--- #{l(:actionview_instancetag_blank_option)} ---", '' ] unless activities.detect(&:is_default)
-
end
-
16
activities.each { |a| collection << [a.name, a.id] }
-
4
collection
-
end
-
-
1
def select_hours(data, criteria, value)
-
if value.to_s.empty?
-
data.select {|row| row[criteria].blank? }
-
else
-
data.select {|row| row[criteria].to_s == value.to_s}
-
end
-
end
-
-
1
def sum_hours(data)
-
sum = 0
-
data.each do |row|
-
sum += row['hours'].to_f
-
end
-
sum
-
end
-
-
1
def options_for_period_select(value)
-
options_for_select([[l(:label_all_time), 'all'],
-
[l(:label_today), 'today'],
-
[l(:label_yesterday), 'yesterday'],
-
[l(:label_this_week), 'current_week'],
-
[l(:label_last_week), 'last_week'],
-
[l(:label_last_n_days, 7), '7_days'],
-
[l(:label_this_month), 'current_month'],
-
[l(:label_last_month), 'last_month'],
-
[l(:label_last_n_days, 30), '30_days'],
-
4
[l(:label_this_year), 'current_year']],
-
value)
-
end
-
-
1
def entries_to_csv(entries)
-
decimal_separator = l(:general_csv_decimal_separator)
-
custom_fields = TimeEntryCustomField.find(:all)
-
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
-
# csv header fields
-
headers = [l(:field_spent_on),
-
l(:field_user),
-
l(:field_activity),
-
l(:field_project),
-
l(:field_issue),
-
l(:field_tracker),
-
l(:field_subject),
-
l(:field_hours),
-
l(:field_comments)
-
]
-
# Export custom fields
-
headers += custom_fields.collect(&:name)
-
-
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
-
c.to_s,
-
l(:general_csv_encoding) ) }
-
# csv lines
-
entries.each do |entry|
-
fields = [format_date(entry.spent_on),
-
entry.user,
-
entry.activity,
-
entry.project,
-
(entry.issue ? entry.issue.id : nil),
-
(entry.issue ? entry.issue.tracker : nil),
-
(entry.issue ? entry.issue.subject : nil),
-
entry.hours.to_s.gsub('.', decimal_separator),
-
entry.comments
-
]
-
fields += custom_fields.collect {|f| show_value(entry.custom_field_values.detect {|v| v.custom_field_id == f.id}) }
-
-
csv << fields.collect {|c| Redmine::CodesetUtil.from_utf8(
-
c.to_s,
-
l(:general_csv_encoding) ) }
-
end
-
end
-
export
-
end
-
-
1
def format_criteria_value(criteria_options, value)
-
if value.blank?
-
"[#{l(:label_none)}]"
-
elsif k = criteria_options[:klass]
-
obj = k.find_by_id(value.to_i)
-
if obj.is_a?(Issue)
-
obj.visible? ? "#{obj.tracker} ##{obj.id}: #{obj.subject}" : "##{obj.id}"
-
else
-
obj
-
end
-
else
-
format_value(value, criteria_options[:format])
-
end
-
end
-
-
1
def report_to_csv(report)
-
decimal_separator = l(:general_csv_decimal_separator)
-
export = FCSV.generate(:col_sep => l(:general_csv_separator)) do |csv|
-
# Column headers
-
headers = report.criteria.collect {|criteria| l(report.available_criteria[criteria][:label]) }
-
headers += report.periods
-
headers << l(:label_total)
-
csv << headers.collect {|c| Redmine::CodesetUtil.from_utf8(
-
c.to_s,
-
l(:general_csv_encoding) ) }
-
# Content
-
report_criteria_to_csv(csv, report.available_criteria, report.columns, report.criteria, report.periods, report.hours)
-
# Total row
-
str_total = Redmine::CodesetUtil.from_utf8(l(:label_total), l(:general_csv_encoding))
-
row = [ str_total ] + [''] * (report.criteria.size - 1)
-
total = 0
-
report.periods.each do |period|
-
sum = sum_hours(select_hours(report.hours, report.columns, period.to_s))
-
total += sum
-
row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
-
end
-
row << ("%.2f" % total).gsub('.',decimal_separator)
-
csv << row
-
end
-
export
-
end
-
-
1
def report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours, level=0)
-
decimal_separator = l(:general_csv_decimal_separator)
-
hours.collect {|h| h[criteria[level]].to_s}.uniq.each do |value|
-
hours_for_value = select_hours(hours, criteria[level], value)
-
next if hours_for_value.empty?
-
row = [''] * level
-
row << Redmine::CodesetUtil.from_utf8(
-
format_criteria_value(available_criteria[criteria[level]], value).to_s,
-
l(:general_csv_encoding) )
-
row += [''] * (criteria.length - level - 1)
-
total = 0
-
periods.each do |period|
-
sum = sum_hours(select_hours(hours_for_value, columns, period.to_s))
-
total += sum
-
row << (sum > 0 ? ("%.2f" % sum).gsub('.',decimal_separator) : '')
-
end
-
row << ("%.2f" % total).gsub('.',decimal_separator)
-
csv << row
-
if criteria.length > level + 1
-
report_criteria_to_csv(csv, available_criteria, columns, criteria, periods, hours_for_value, level + 1)
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module TrackersHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module UsersHelper
-
1
def users_status_options_for_select(selected)
-
user_count_by_status = User.count(:group => 'status').to_hash
-
options_for_select([[l(:label_all), ''],
-
["#{l(:status_active)} (#{user_count_by_status[1].to_i})", '1'],
-
["#{l(:status_registered)} (#{user_count_by_status[2].to_i})", '2'],
-
["#{l(:status_locked)} (#{user_count_by_status[3].to_i})", '3']], selected.to_s)
-
end
-
-
# Options for the new membership projects combo-box
-
1
def options_for_membership_project_select(user, projects)
-
options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
-
options << project_tree_options_for_select(projects) do |p|
-
{:disabled => (user.projects.include?(p))}
-
end
-
options
-
end
-
-
1
def user_mail_notification_options(user)
-
user.valid_notification_options.collect {|o| [l(o.last), o.first]}
-
end
-
-
1
def change_status_link(user)
-
url = {:controller => 'users', :action => 'update', :id => user, :page => params[:page], :status => params[:status], :tab => nil}
-
-
if user.locked?
-
link_to l(:button_unlock), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
-
elsif user.registered?
-
link_to l(:button_activate), url.merge(:user => {:status => User::STATUS_ACTIVE}), :method => :put, :class => 'icon icon-unlock'
-
elsif user != User.current
-
link_to l(:button_lock), url.merge(:user => {:status => User::STATUS_LOCKED}), :method => :put, :class => 'icon icon-lock'
-
end
-
end
-
-
1
def user_settings_tabs
-
tabs = [{:name => 'general', :partial => 'users/general', :label => :label_general},
-
{:name => 'memberships', :partial => 'users/memberships', :label => :label_project_plural}
-
]
-
if Group.all.any?
-
tabs.insert 1, {:name => 'groups', :partial => 'users/groups', :label => :label_group_plural}
-
end
-
tabs
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module VersionsHelper
-
-
1
STATUS_BY_CRITERIAS = %w(category tracker status priority author assigned_to)
-
-
1
def render_issue_status_by(version, criteria)
-
criteria = 'category' unless STATUS_BY_CRITERIAS.include?(criteria)
-
-
h = Hash.new {|k,v| k[v] = [0, 0]}
-
begin
-
# Total issue count
-
Issue.count(:group => criteria,
-
:conditions => ["#{Issue.table_name}.fixed_version_id = ?", version.id]).each {|c,s| h[c][0] = s}
-
# Open issues count
-
Issue.count(:group => criteria,
-
:include => :status,
-
:conditions => ["#{Issue.table_name}.fixed_version_id = ? AND #{IssueStatus.table_name}.is_closed = ?", version.id, false]).each {|c,s| h[c][1] = s}
-
rescue ActiveRecord::RecordNotFound
-
# When grouping by an association, Rails throws this exception if there's no result (bug)
-
end
-
counts = h.keys.compact.sort.collect {|k| {:group => k, :total => h[k][0], :open => h[k][1], :closed => (h[k][0] - h[k][1])}}
-
max = counts.collect {|c| c[:total]}.max
-
-
render :partial => 'issue_counts', :locals => {:version => version, :criteria => criteria, :counts => counts, :max => max}
-
end
-
-
1
def status_by_options_for_select(value)
-
options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WatchersHelper
-
-
1
def watcher_tag(object, user, options={})
-
6
content_tag("span", watcher_link(object, user), :class => watcher_css(object))
-
end
-
-
1
def watcher_link(object, user)
-
6
return '' unless user && user.logged? && object.respond_to?('watched_by?')
-
6
watched = object.watched_by?(user)
-
6
url = {:controller => 'watchers',
-
6
:action => (watched ? 'unwatch' : 'watch'),
-
:object_type => object.class.to_s.underscore,
-
:object_id => object.id}
-
6
link_to_remote((watched ? l(:button_unwatch) : l(:button_watch)),
-
{:url => url},
-
:href => url_for(url),
-
6
:class => (watched ? 'icon icon-fav' : 'icon icon-fav-off'))
-
-
end
-
-
# Returns the css class used to identify watch links for a given +object+
-
1
def watcher_css(object)
-
6
"#{object.class.to_s.underscore}-#{object.id}-watcher"
-
end
-
-
# Returns a comma separated list of users watching the given object
-
1
def watchers_list(object)
-
2
remove_allowed = User.current.allowed_to?("delete_#{object.class.name.underscore}_watchers".to_sym, object.project)
-
2
content = ''.html_safe
-
2
lis = object.watcher_users.collect do |user|
-
s = ''.html_safe
-
s << avatar(user, :size => "16").to_s
-
s << link_to_user(user, :class => 'user')
-
if remove_allowed
-
url = {:controller => 'watchers',
-
:action => 'destroy',
-
:object_type => object.class.to_s.underscore,
-
:object_id => object.id,
-
:user_id => user}
-
s << ' '
-
s << link_to_remote(image_tag('delete.png'),
-
{:url => url},
-
:href => url_for(url),
-
:style => "vertical-align: middle",
-
:class => "delete")
-
end
-
content << content_tag('li', s)
-
end
-
2
content.present? ? content_tag('ul', content) : content
-
end
-
-
1
def watchers_checkboxes(object, users, checked=nil)
-
2
users.map do |user|
-
4
c = checked.nil? ? object.watched_by?(user) : checked
-
4
tag = check_box_tag 'issue[watcher_user_ids][]', user.id, c, :id => nil
-
4
content_tag 'label', "#{tag} #{h(user)}".html_safe,
-
:id => "issue_watcher_user_ids_#{user.id}",
-
:class => "floating"
-
end.join.html_safe
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WelcomeHelper
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WikiHelper
-
-
1
def wiki_page_options_for_select(pages, selected = nil, parent = nil, level = 0)
-
pages = pages.group_by(&:parent) unless pages.is_a?(Hash)
-
s = ''.html_safe
-
if pages.has_key?(parent)
-
pages[parent].each do |page|
-
attrs = "value='#{page.id}'"
-
attrs << " selected='selected'" if selected == page
-
indent = (level > 0) ? (' ' * level * 2 + '» ') : ''
-
-
s << content_tag('option', (indent + h(page.pretty_title)).html_safe, :value => page.id.to_s, :selected => selected == page) +
-
wiki_page_options_for_select(pages, selected, page, level + 1)
-
end
-
end
-
s
-
end
-
-
1
def wiki_page_breadcrumb(page)
-
breadcrumb(page.ancestors.reverse.collect {|parent|
-
link_to(h(parent.pretty_title), {:controller => 'wiki', :action => 'show', :id => parent.title, :project_id => parent.project})
-
})
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module WorkflowsHelper
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require "digest/md5"
-
-
1
class Attachment < ActiveRecord::Base
-
1
belongs_to :container, :polymorphic => true
-
1
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
-
-
1
validates_presence_of :filename, :author
-
1
validates_length_of :filename, :maximum => 255
-
1
validates_length_of :disk_filename, :maximum => 255
-
1
validate :validate_max_file_size
-
-
acts_as_event :title => :filename,
-
1
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
-
-
acts_as_activity_provider :type => 'files',
-
:permission => :view_files,
-
:author_key => :author_id,
-
:find_options => {:select => "#{Attachment.table_name}.*",
-
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
-
1
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
-
-
acts_as_activity_provider :type => 'documents',
-
:permission => :view_documents,
-
:author_key => :author_id,
-
:find_options => {:select => "#{Attachment.table_name}.*",
-
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
-
1
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
-
-
1
cattr_accessor :storage_path
-
1
@@storage_path = Redmine::Configuration['attachments_storage_path'] || File.join(Rails.root, "files")
-
-
1
before_save :files_to_final_location
-
1
after_destroy :delete_from_disk
-
-
# Returns an unsaved copy of the attachment
-
1
def copy(attributes=nil)
-
copy = self.class.new
-
copy.attributes = self.attributes.dup.except("id", "downloads")
-
copy.attributes = attributes if attributes
-
copy
-
end
-
-
1
def validate_max_file_size
-
if @temp_file && self.filesize > Setting.attachment_max_size.to_i.kilobytes
-
errors.add(:base, l(:error_attachment_too_big, :max_size => Setting.attachment_max_size.to_i.kilobytes))
-
end
-
end
-
-
1
def file=(incoming_file)
-
unless incoming_file.nil?
-
@temp_file = incoming_file
-
if @temp_file.size > 0
-
if @temp_file.respond_to?(:original_filename)
-
self.filename = @temp_file.original_filename
-
self.filename.force_encoding("UTF-8") if filename.respond_to?(:force_encoding)
-
end
-
if @temp_file.respond_to?(:content_type)
-
self.content_type = @temp_file.content_type.to_s.chomp
-
end
-
if content_type.blank? && filename.present?
-
self.content_type = Redmine::MimeType.of(filename)
-
end
-
self.filesize = @temp_file.size
-
end
-
end
-
end
-
-
1
def file
-
nil
-
end
-
-
1
def filename=(arg)
-
write_attribute :filename, sanitize_filename(arg.to_s)
-
if new_record? && disk_filename.blank?
-
self.disk_filename = Attachment.disk_filename(filename)
-
end
-
filename
-
end
-
-
# Copies the temporary file to its final location
-
# and computes its MD5 hash
-
1
def files_to_final_location
-
if @temp_file && (@temp_file.size > 0)
-
logger.info("Saving attachment '#{self.diskfile}' (#{@temp_file.size} bytes)")
-
md5 = Digest::MD5.new
-
File.open(diskfile, "wb") do |f|
-
if @temp_file.respond_to?(:read)
-
buffer = ""
-
while (buffer = @temp_file.read(8192))
-
f.write(buffer)
-
md5.update(buffer)
-
end
-
else
-
f.write(@temp_file)
-
md5.update(@temp_file)
-
end
-
end
-
self.digest = md5.hexdigest
-
end
-
@temp_file = nil
-
# Don't save the content type if it's longer than the authorized length
-
if self.content_type && self.content_type.length > 255
-
self.content_type = nil
-
end
-
end
-
-
# Deletes the file from the file system if it's not referenced by other attachments
-
1
def delete_from_disk
-
1346
if Attachment.first(:conditions => ["disk_filename = ? AND id <> ?", disk_filename, id]).nil?
-
753
delete_from_disk!
-
end
-
end
-
-
# Returns file's location on disk
-
1
def diskfile
-
753
File.join(self.class.storage_path, disk_filename.to_s)
-
end
-
-
1
def increment_download
-
increment!(:downloads)
-
end
-
-
1
def project
-
container.try(:project)
-
end
-
-
1
def visible?(user=User.current)
-
container && container.attachments_visible?(user)
-
end
-
-
1
def deletable?(user=User.current)
-
container && container.attachments_deletable?(user)
-
end
-
-
1
def image?
-
self.filename =~ /\.(bmp|gif|jpg|jpe|jpeg|png)$/i
-
end
-
-
1
def is_text?
-
Redmine::MimeType.is_type?('text', filename)
-
end
-
-
1
def is_diff?
-
self.filename =~ /\.(patch|diff)$/i
-
end
-
-
# Returns true if the file is readable
-
1
def readable?
-
File.readable?(diskfile)
-
end
-
-
# Returns the attachment token
-
1
def token
-
"#{id}.#{digest}"
-
end
-
-
# Finds an attachment that matches the given token and that has no container
-
1
def self.find_by_token(token)
-
if token.to_s =~ /^(\d+)\.([0-9a-f]+)$/
-
attachment_id, attachment_digest = $1, $2
-
attachment = Attachment.first(:conditions => {:id => attachment_id, :digest => attachment_digest})
-
if attachment && attachment.container.nil?
-
attachment
-
end
-
end
-
end
-
-
# Bulk attaches a set of files to an object
-
#
-
# Returns a Hash of the results:
-
# :files => array of the attached files
-
# :unsaved => array of the files that could not be attached
-
1
def self.attach_files(obj, attachments)
-
result = obj.save_attachments(attachments, User.current)
-
obj.attach_saved_attachments
-
result
-
end
-
-
1
def self.latest_attach(attachments, filename)
-
attachments.sort_by(&:created_on).reverse.detect {
-
|att| att.filename.downcase == filename.downcase
-
}
-
end
-
-
1
def self.prune(age=1.day)
-
attachments = Attachment.all(:conditions => ["created_on < ? AND (container_type IS NULL OR container_type = '')", Time.now - age])
-
attachments.each(&:destroy)
-
end
-
-
1
private
-
-
# Physically deletes the file from the file system
-
1
def delete_from_disk!
-
753
if disk_filename.present? && File.exist?(diskfile)
-
File.delete(diskfile)
-
end
-
end
-
-
1
def sanitize_filename(value)
-
# get only the filename, not the whole path
-
just_filename = value.gsub(/^.*(\\|\/)/, '')
-
-
# Finally, replace invalid characters with underscore
-
@filename = just_filename.gsub(/[\/\?\%\*\:\|\"\'<>]+/, '_')
-
end
-
-
# Returns an ASCII or hashed filename
-
1
def self.disk_filename(filename)
-
timestamp = DateTime.now.strftime("%y%m%d%H%M%S")
-
ascii = ''
-
if filename =~ %r{^[a-zA-Z0-9_\.\-]*$}
-
ascii = filename
-
else
-
ascii = Digest::MD5.hexdigest(filename)
-
# keep the extension if any
-
ascii << $1 if filename =~ %r{(\.[a-zA-Z0-9]+)$}
-
end
-
while File.exist?(File.join(@@storage_path, "#{timestamp}_#{ascii}"))
-
timestamp.succ!
-
end
-
"#{timestamp}_#{ascii}"
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
# Generic exception for when the AuthSource can not be reached
-
# (eg. can not connect to the LDAP)
-
1
class AuthSourceException < Exception; end
-
-
1
class AuthSource < ActiveRecord::Base
-
1
include Redmine::SubclassFactory
-
1
include Redmine::Ciphering
-
-
1
has_many :users
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 60
-
-
1
def authenticate(login, password)
-
end
-
-
1
def test_connection
-
end
-
-
1
def auth_method_name
-
"Abstract"
-
end
-
-
1
def account_password
-
read_ciphered_attribute(:account_password)
-
end
-
-
1
def account_password=(arg)
-
write_ciphered_attribute(:account_password, arg)
-
end
-
-
1
def allow_password_changes?
-
self.class.allow_password_changes?
-
end
-
-
# Does this auth source backend allow password changes?
-
1
def self.allow_password_changes?
-
false
-
end
-
-
# Try to authenticate a user not yet registered against available sources
-
1
def self.authenticate(login, password)
-
AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
-
begin
-
logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
-
attrs = source.authenticate(login, password)
-
rescue => e
-
logger.error "Error during authentication: #{e.message}"
-
attrs = nil
-
end
-
return attrs if attrs
-
end
-
return nil
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
1
require 'net/ldap'
-
1
require 'net/ldap/dn'
-
-
1
class AuthSourceLdap < AuthSource
-
1
validates_presence_of :host, :port, :attr_login
-
1
validates_length_of :name, :host, :maximum => 60, :allow_nil => true
-
1
validates_length_of :account, :account_password, :base_dn, :filter, :maximum => 255, :allow_blank => true
-
1
validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
-
1
validates_numericality_of :port, :only_integer => true
-
1
validate :validate_filter
-
-
1
before_validation :strip_ldap_attributes
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "filter"
-
attr_name = "ldap_filter"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def initialize(attributes=nil, *args)
-
super
-
self.port = 389 if self.port == 0
-
end
-
-
1
def authenticate(login, password)
-
return nil if login.blank? || password.blank?
-
attrs = get_user_dn(login, password)
-
-
if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
-
logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
-
return attrs.except(:dn)
-
end
-
rescue Net::LDAP::LdapError => e
-
raise AuthSourceException.new(e.message)
-
end
-
-
# test the connection to the LDAP
-
1
def test_connection
-
ldap_con = initialize_ldap_con(self.account, self.account_password)
-
ldap_con.open { }
-
rescue Net::LDAP::LdapError => e
-
raise "LdapError: " + e.message
-
end
-
-
1
def auth_method_name
-
"LDAP"
-
end
-
-
1
private
-
-
1
def ldap_filter
-
if filter.present?
-
Net::LDAP::Filter.construct(filter)
-
end
-
rescue Net::LDAP::LdapError
-
nil
-
end
-
-
1
def validate_filter
-
if filter.present? && ldap_filter.nil?
-
errors.add(:filter, :invalid)
-
end
-
end
-
-
1
def strip_ldap_attributes
-
[:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
-
write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
-
end
-
end
-
-
1
def initialize_ldap_con(ldap_user, ldap_password)
-
options = { :host => self.host,
-
:port => self.port,
-
:encryption => (self.tls ? :simple_tls : nil)
-
}
-
options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
-
Net::LDAP.new options
-
end
-
-
1
def get_user_attributes_from_ldap_entry(entry)
-
{
-
:dn => entry.dn,
-
:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
-
:lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
-
:mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
-
:auth_source_id => self.id
-
}
-
end
-
-
# Return the attributes needed for the LDAP search. It will only
-
# include the user attributes if on-the-fly registration is enabled
-
1
def search_attributes
-
if onthefly_register?
-
['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
-
else
-
['dn']
-
end
-
end
-
-
# Check if a DN (user record) authenticates with the password
-
1
def authenticate_dn(dn, password)
-
if dn.present? && password.present?
-
initialize_ldap_con(dn, password).bind
-
end
-
end
-
-
# Get the user's dn and any attributes for them, given their login
-
1
def get_user_dn(login, password)
-
ldap_con = nil
-
if self.account && self.account.include?("$login")
-
ldap_con = initialize_ldap_con(self.account.sub("$login", Net::LDAP::DN.escape(login)), password)
-
else
-
ldap_con = initialize_ldap_con(self.account, self.account_password)
-
end
-
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
-
object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
-
attrs = {}
-
-
search_filter = object_filter & login_filter
-
if f = ldap_filter
-
search_filter = search_filter & f
-
end
-
-
ldap_con.search( :base => self.base_dn,
-
:filter => search_filter,
-
:attributes=> search_attributes) do |entry|
-
-
if onthefly_register?
-
attrs = get_user_attributes_from_ldap_entry(entry)
-
else
-
attrs = {:dn => entry.dn}
-
end
-
-
logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
-
end
-
-
attrs
-
end
-
-
1
def self.get_attr(entry, attr_name)
-
if !attr_name.blank?
-
entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Board < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
has_many :topics, :class_name => 'Message', :conditions => "#{Message.table_name}.parent_id IS NULL", :order => "#{Message.table_name}.created_on DESC"
-
1
has_many :messages, :dependent => :destroy, :order => "#{Message.table_name}.created_on DESC"
-
1
belongs_to :last_message, :class_name => 'Message', :foreign_key => :last_message_id
-
1
acts_as_list :scope => :project_id
-
1
acts_as_watchable
-
-
1
validates_presence_of :name, :description
-
1
validates_length_of :name, :maximum => 30
-
1
validates_length_of :description, :maximum => 255
-
-
1
scope :visible, lambda {|*args| { :include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
-
-
1
safe_attributes 'name', 'description', 'move_to'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_messages, project)
-
end
-
-
1
def to_s
-
name
-
end
-
-
1
def reset_counters!
-
self.class.reset_counters!(id)
-
end
-
-
# Updates topics_count, messages_count and last_message_id attributes for +board_id+
-
1
def self.reset_counters!(board_id)
-
board_id = board_id.to_i
-
update_all("topics_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id} AND parent_id IS NULL)," +
-
" messages_count = (SELECT COUNT(*) FROM #{Message.table_name} WHERE board_id=#{board_id})," +
-
" last_message_id = (SELECT MAX(id) FROM #{Message.table_name} WHERE board_id=#{board_id})",
-
["id = ?", board_id])
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Change < ActiveRecord::Base
-
1
belongs_to :changeset
-
-
1
validates_presence_of :changeset_id, :action, :path
-
1
before_save :init_path
-
1
before_validation :replace_invalid_utf8_of_path
-
-
1
def relative_path
-
changeset.repository.relative_path(path)
-
end
-
-
1
def replace_invalid_utf8_of_path
-
self.path = Redmine::CodesetUtil.replace_invalid_utf8(self.path)
-
self.from_path = Redmine::CodesetUtil.replace_invalid_utf8(self.from_path)
-
end
-
-
1
def init_path
-
self.path ||= ""
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
-
1
class Changeset < ActiveRecord::Base
-
1
belongs_to :repository
-
1
belongs_to :user
-
1
has_many :filechanges, :class_name => 'Change', :dependent => :delete_all
-
1
has_and_belongs_to_many :issues
-
1
has_and_belongs_to_many :parents,
-
:class_name => "Changeset",
-
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
-
:association_foreign_key => 'parent_id', :foreign_key => 'changeset_id'
-
1
has_and_belongs_to_many :children,
-
:class_name => "Changeset",
-
:join_table => "#{table_name_prefix}changeset_parents#{table_name_suffix}",
-
:association_foreign_key => 'changeset_id', :foreign_key => 'parent_id'
-
-
acts_as_event :title => Proc.new {|o| o.title},
-
:description => :long_comments,
-
:datetime => :committed_on,
-
1
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :repository_id => o.repository.identifier_param, :rev => o.identifier}}
-
-
acts_as_searchable :columns => 'comments',
-
:include => {:repository => :project},
-
:project_key => "#{Repository.table_name}.project_id",
-
1
:date_column => 'committed_on'
-
-
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
-
:author_key => :user_id,
-
1
:find_options => {:include => [:user, {:repository => :project}]}
-
-
1
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
-
1
validates_uniqueness_of :revision, :scope => :repository_id
-
1
validates_uniqueness_of :scmid, :scope => :repository_id, :allow_nil => true
-
-
1
scope :visible,
-
lambda {|*args| { :include => {:repository => :project},
-
2
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_changesets, *args) } }
-
-
1
after_create :scan_for_issues
-
1
before_create :before_create_cs
-
-
1
def revision=(r)
-
write_attribute :revision, (r.nil? ? nil : r.to_s)
-
end
-
-
# Returns the identifier of this changeset; depending on repository backends
-
1
def identifier
-
if repository.class.respond_to? :changeset_identifier
-
repository.class.changeset_identifier self
-
else
-
revision.to_s
-
end
-
end
-
-
1
def committed_on=(date)
-
self.commit_date = date
-
super
-
end
-
-
# Returns the readable identifier
-
1
def format_identifier
-
if repository.class.respond_to? :format_changeset_identifier
-
repository.class.format_changeset_identifier self
-
else
-
identifier
-
end
-
end
-
-
1
def project
-
repository.project
-
end
-
-
1
def author
-
user || committer.to_s.split('<').first
-
end
-
-
1
def before_create_cs
-
self.committer = self.class.to_utf8(self.committer, repository.repo_log_encoding)
-
self.comments = self.class.normalize_comments(
-
self.comments, repository.repo_log_encoding)
-
self.user = repository.find_committer_user(self.committer)
-
end
-
-
1
def scan_for_issues
-
scan_comment_for_issue_ids
-
end
-
-
1
TIMELOG_RE = /
-
(
-
((\d+)(h|hours?))((\d+)(m|min)?)?
-
|
-
((\d+)(h|hours?|m|min))
-
|
-
(\d+):(\d+)
-
|
-
(\d+([\.,]\d+)?)h?
-
)
-
/x
-
-
1
def scan_comment_for_issue_ids
-
return if comments.blank?
-
# keywords used to reference issues
-
ref_keywords = Setting.commit_ref_keywords.downcase.split(",").collect(&:strip)
-
ref_keywords_any = ref_keywords.delete('*')
-
# keywords used to fix issues
-
fix_keywords = Setting.commit_fix_keywords.downcase.split(",").collect(&:strip)
-
-
kw_regexp = (ref_keywords + fix_keywords).collect{|kw| Regexp.escape(kw)}.join("|")
-
-
referenced_issues = []
-
-
comments.scan(/([\s\(\[,-]|^)((#{kw_regexp})[\s:]+)?(#\d+(\s+@#{TIMELOG_RE})?([\s,;&]+#\d+(\s+@#{TIMELOG_RE})?)*)(?=[[:punct:]]|\s|<|$)/i) do |match|
-
action, refs = match[2], match[3]
-
next unless action.present? || ref_keywords_any
-
-
refs.scan(/#(\d+)(\s+@#{TIMELOG_RE})?/).each do |m|
-
issue, hours = find_referenced_issue_by_id(m[0].to_i), m[2]
-
if issue
-
referenced_issues << issue
-
fix_issue(issue) if fix_keywords.include?(action.to_s.downcase)
-
log_time(issue, hours) if hours && Setting.commit_logtime_enabled?
-
end
-
end
-
end
-
-
referenced_issues.uniq!
-
self.issues = referenced_issues unless referenced_issues.empty?
-
end
-
-
1
def short_comments
-
@short_comments || split_comments.first
-
end
-
-
1
def long_comments
-
@long_comments || split_comments.last
-
end
-
-
1
def text_tag(ref_project=nil)
-
tag = if scmid?
-
"commit:#{scmid}"
-
else
-
"r#{revision}"
-
end
-
if repository && repository.identifier.present?
-
tag = "#{repository.identifier}|#{tag}"
-
end
-
if ref_project && project && ref_project != project
-
tag = "#{project.identifier}:#{tag}"
-
end
-
tag
-
end
-
-
# Returns the title used for the changeset in the activity/search results
-
1
def title
-
repo = (repository && repository.identifier.present?) ? " (#{repository.identifier})" : ''
-
comm = short_comments.blank? ? '' : (': ' + short_comments)
-
"#{l(:label_revision)} #{format_identifier}#{repo}#{comm}"
-
end
-
-
# Returns the previous changeset
-
1
def previous
-
@previous ||= Changeset.find(:first,
-
:conditions => ['id < ? AND repository_id = ?',
-
self.id, self.repository_id],
-
:order => 'id DESC')
-
end
-
-
# Returns the next changeset
-
1
def next
-
@next ||= Changeset.find(:first,
-
:conditions => ['id > ? AND repository_id = ?',
-
self.id, self.repository_id],
-
:order => 'id ASC')
-
end
-
-
# Creates a new Change from it's common parameters
-
1
def create_change(change)
-
Change.create(:changeset => self,
-
:action => change[:action],
-
:path => change[:path],
-
:from_path => change[:from_path],
-
:from_revision => change[:from_revision])
-
end
-
-
# Finds an issue that can be referenced by the commit message
-
1
def find_referenced_issue_by_id(id)
-
return nil if id.blank?
-
issue = Issue.find_by_id(id.to_i, :include => :project)
-
if Setting.commit_cross_project_ref?
-
# all issues can be referenced/fixed
-
elsif issue
-
# issue that belong to the repository project, a subproject or a parent project only
-
unless issue.project &&
-
(project == issue.project || project.is_ancestor_of?(issue.project) ||
-
project.is_descendant_of?(issue.project))
-
issue = nil
-
end
-
end
-
issue
-
end
-
-
1
private
-
-
1
def fix_issue(issue)
-
status = IssueStatus.find_by_id(Setting.commit_fix_status_id.to_i)
-
if status.nil?
-
logger.warn("No status matches commit_fix_status_id setting (#{Setting.commit_fix_status_id})") if logger
-
return issue
-
end
-
-
# the issue may have been updated by the closure of another one (eg. duplicate)
-
issue.reload
-
# don't change the status is the issue is closed
-
return if issue.status && issue.status.is_closed?
-
-
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag(issue.project)))
-
issue.status = status
-
unless Setting.commit_fix_done_ratio.blank?
-
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
-
end
-
Redmine::Hook.call_hook(:model_changeset_scan_commit_for_issue_ids_pre_issue_update,
-
{ :changeset => self, :issue => issue })
-
unless issue.save
-
logger.warn("Issue ##{issue.id} could not be saved by changeset #{id}: #{issue.errors.full_messages}") if logger
-
end
-
issue
-
end
-
-
1
def log_time(issue, hours)
-
time_entry = TimeEntry.new(
-
:user => user,
-
:hours => hours,
-
:issue => issue,
-
:spent_on => commit_date,
-
:comments => l(:text_time_logged_by_changeset, :value => text_tag(issue.project),
-
:locale => Setting.default_language)
-
)
-
time_entry.activity = log_time_activity unless log_time_activity.nil?
-
-
unless time_entry.save
-
logger.warn("TimeEntry could not be created by changeset #{id}: #{time_entry.errors.full_messages}") if logger
-
end
-
time_entry
-
end
-
-
1
def log_time_activity
-
if Setting.commit_logtime_activity_id.to_i > 0
-
TimeEntryActivity.find_by_id(Setting.commit_logtime_activity_id.to_i)
-
end
-
end
-
-
1
def split_comments
-
comments =~ /\A(.+?)\r?\n(.*)$/m
-
@short_comments = $1 || comments
-
@long_comments = $2.to_s.strip
-
return @short_comments, @long_comments
-
end
-
-
1
public
-
-
# Strips and reencodes a commit log before insertion into the database
-
1
def self.normalize_comments(str, encoding)
-
Changeset.to_utf8(str.to_s.strip, encoding)
-
end
-
-
1
def self.to_utf8(str, encoding)
-
Redmine::CodesetUtil.to_utf8(str, encoding)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Comment < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :commented, :polymorphic => true, :counter_cache => true
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
-
1
validates_presence_of :commented, :author, :comments
-
-
1
safe_attributes 'comments'
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CommentObserver < ActiveRecord::Observer
-
1
def after_create(comment)
-
if comment.commented.is_a?(News) && Setting.notified_events.include?('news_comment_added')
-
Mailer.news_comment_added(comment).deliver
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomField < ActiveRecord::Base
-
1
include Redmine::SubclassFactory
-
-
1
has_many :custom_values, :dependent => :delete_all
-
1
acts_as_list :scope => 'type = \'#{self.class}\''
-
1
serialize :possible_values
-
-
1
validates_presence_of :name, :field_format
-
1
validates_uniqueness_of :name, :scope => :type
-
1
validates_length_of :name, :maximum => 30
-
1
validates_inclusion_of :field_format, :in => Redmine::CustomFieldFormat.available_formats
-
-
1
validate :validate_custom_field
-
1
before_validation :set_searchable
-
-
1
def initialize(attributes=nil, *args)
-
super
-
self.possible_values ||= []
-
end
-
-
1
def set_searchable
-
# make sure these fields are not searchable
-
self.searchable = false if %w(int float date bool).include?(field_format)
-
# make sure only these fields can have multiple values
-
self.multiple = false unless %w(list user version).include?(field_format)
-
true
-
end
-
-
1
def validate_custom_field
-
if self.field_format == "list"
-
errors.add(:possible_values, :blank) if self.possible_values.nil? || self.possible_values.empty?
-
errors.add(:possible_values, :invalid) unless self.possible_values.is_a? Array
-
end
-
-
if regexp.present?
-
begin
-
Regexp.new(regexp)
-
rescue
-
errors.add(:regexp, :invalid)
-
end
-
end
-
-
if default_value.present? && !valid_field_value?(default_value)
-
errors.add(:default_value, :invalid)
-
end
-
end
-
-
1
def possible_values_options(obj=nil)
-
case field_format
-
when 'user', 'version'
-
if obj.respond_to?(:project) && obj.project
-
case field_format
-
when 'user'
-
obj.project.users.sort.collect {|u| [u.to_s, u.id.to_s]}
-
when 'version'
-
obj.project.shared_versions.sort.collect {|u| [u.to_s, u.id.to_s]}
-
end
-
elsif obj.is_a?(Array)
-
obj.collect {|o| possible_values_options(o)}.reduce(:&)
-
else
-
[]
-
end
-
when 'bool'
-
[[l(:general_text_Yes), '1'], [l(:general_text_No), '0']]
-
else
-
possible_values || []
-
end
-
end
-
-
1
def possible_values(obj=nil)
-
23
case field_format
-
when 'user', 'version'
-
possible_values_options(obj).collect(&:last)
-
when 'bool'
-
['1', '0']
-
else
-
23
values = super()
-
23
if values.is_a?(Array)
-
23
values.each do |value|
-
69
value.force_encoding('UTF-8') if value.respond_to?(:force_encoding)
-
end
-
end
-
23
values
-
end
-
end
-
-
# Makes possible_values accept a multiline string
-
1
def possible_values=(arg)
-
if arg.is_a?(Array)
-
super(arg.compact.collect(&:strip).select {|v| !v.blank?})
-
else
-
self.possible_values = arg.to_s.split(/[\n\r]+/)
-
end
-
end
-
-
1
def cast_value(value)
-
casted = nil
-
unless value.blank?
-
case field_format
-
when 'string', 'text', 'list'
-
casted = value
-
when 'date'
-
casted = begin; value.to_date; rescue; nil end
-
when 'bool'
-
casted = (value == '1' ? true : false)
-
when 'int'
-
casted = value.to_i
-
when 'float'
-
casted = value.to_f
-
when 'user', 'version'
-
casted = (value.blank? ? nil : field_format.classify.constantize.find_by_id(value.to_i))
-
end
-
end
-
casted
-
end
-
-
# Returns a ORDER BY clause that can used to sort customized
-
# objects by their value of the custom field.
-
# Returns false, if the custom field can not be used for sorting.
-
1
def order_statement
-
176
return nil if multiple?
-
176
case field_format
-
when 'string', 'text', 'list', 'date', 'bool'
-
# COALESCE is here to make sure that blank and NULL values are sorted equally
-
"COALESCE((SELECT cv_sort.value FROM #{CustomValue.table_name} cv_sort" +
-
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
-
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
-
154
" AND cv_sort.custom_field_id=#{id} LIMIT 1), '')"
-
when 'int', 'float'
-
# Make the database cast values into numeric
-
# Postgresql will raise an error if a value can not be casted!
-
# CustomValue validations should ensure that it doesn't occur
-
"(SELECT CAST(cv_sort.value AS decimal(60,3)) FROM #{CustomValue.table_name} cv_sort" +
-
" WHERE cv_sort.customized_type='#{self.class.customized_class.name}'" +
-
" AND cv_sort.customized_id=#{self.class.customized_class.table_name}.id" +
-
22
" AND cv_sort.custom_field_id=#{id} AND cv_sort.value <> '' AND cv_sort.value IS NOT NULL LIMIT 1)"
-
else
-
nil
-
end
-
end
-
-
1
def <=>(field)
-
5475
position <=> field.position
-
end
-
-
1
def self.customized_class
-
352
self.name =~ /^(.+)CustomField$/
-
352
begin; $1.constantize; rescue nil; end
-
end
-
-
# to move in project_custom_field
-
1
def self.for_all
-
1463
find(:all, :conditions => ["is_for_all=?", true], :order => 'position')
-
end
-
-
1
def type_name
-
nil
-
end
-
-
# Returns the error messages for the given value
-
# or an empty array if value is a valid value for the custom field
-
1
def validate_field_value(value)
-
48
errs = []
-
48
if value.is_a?(Array)
-
if !multiple?
-
errs << ::I18n.t('activerecord.errors.messages.invalid')
-
end
-
if is_required? && value.detect(&:present?).nil?
-
errs << ::I18n.t('activerecord.errors.messages.blank')
-
end
-
value.each {|v| errs += validate_field_value_format(v)}
-
else
-
48
if is_required? && value.blank?
-
errs << ::I18n.t('activerecord.errors.messages.blank')
-
end
-
48
errs += validate_field_value_format(value)
-
end
-
48
errs
-
end
-
-
# Returns true if value is a valid value for the custom field
-
1
def valid_field_value?(value)
-
validate_field_value(value).empty?
-
end
-
-
1
protected
-
-
# Returns the error message for the given value regarding its format
-
1
def validate_field_value_format(value)
-
48
errs = []
-
48
if value.present?
-
2
errs << ::I18n.t('activerecord.errors.messages.invalid') unless regexp.blank? or value =~ Regexp.new(regexp)
-
2
errs << ::I18n.t('activerecord.errors.messages.too_short', :count => min_length) if min_length > 0 and value.length < min_length
-
2
errs << ::I18n.t('activerecord.errors.messages.too_long', :count => max_length) if max_length > 0 and value.length > max_length
-
-
# Format specific validations
-
2
case field_format
-
when 'int'
-
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value =~ /^[+-]?\d+$/
-
when 'float'
-
begin; Kernel.Float(value); rescue; errs << ::I18n.t('activerecord.errors.messages.invalid') end
-
when 'date'
-
errs << ::I18n.t('activerecord.errors.messages.not_a_date') unless value =~ /^\d{4}-\d{2}-\d{2}$/ && begin; value.to_date; rescue; false end
-
when 'list'
-
errs << ::I18n.t('activerecord.errors.messages.inclusion') unless possible_values.include?(value)
-
end
-
end
-
48
errs
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomFieldValue
-
1
attr_accessor :custom_field, :customized, :value
-
-
1
def custom_field_id
-
2
custom_field.id
-
end
-
-
1
def true?
-
4
self.value == '1'
-
end
-
-
1
def editable?
-
custom_field.editable?
-
end
-
-
1
def visible?
-
67
custom_field.visible?
-
end
-
-
1
def required?
-
custom_field.is_required?
-
end
-
-
1
def to_s
-
value.to_s
-
end
-
-
1
def validate_value
-
48
custom_field.validate_field_value(value).each do |message|
-
customized.errors.add(:base, custom_field.name + ' ' + message)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class CustomValue < ActiveRecord::Base
-
1
belongs_to :custom_field
-
1
belongs_to :customized, :polymorphic => true
-
-
1
def initialize(attributes=nil, *args)
-
243
super
-
243
if new_record? && custom_field && (customized_type.blank? || (customized && customized.new_record?))
-
52
self.value ||= custom_field.default_value
-
end
-
end
-
-
# Returns true if the boolean custom value is true
-
1
def true?
-
self.value == '1'
-
end
-
-
1
def editable?
-
custom_field.editable?
-
end
-
-
1
def visible?
-
custom_field.visible?
-
end
-
-
1
def required?
-
custom_field.is_required?
-
end
-
-
1
def to_s
-
value.to_s
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Document < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
-
1
acts_as_attachable :delete_permission => :manage_documents
-
-
1
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
-
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
-
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
-
1
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
-
1
acts_as_activity_provider :find_options => {:include => :project}
-
-
1
validates_presence_of :project, :title, :category
-
1
validates_length_of :title, :maximum => 60
-
-
1
scope :visible, lambda {|*args| { :include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_documents, *args) } }
-
-
1
safe_attributes 'category_id', 'title', 'description'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_documents, project)
-
end
-
-
1
def initialize(attributes=nil, *args)
-
super
-
if new_record?
-
self.category ||= DocumentCategory.default
-
end
-
end
-
-
1
def updated_on
-
unless @updated_on
-
a = attachments.last
-
@updated_on = (a && a.created_on) || created_on
-
end
-
@updated_on
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentCategory < Enumeration
-
1
has_many :documents, :foreign_key => 'category_id'
-
-
1
OptionName = :enumeration_doc_categories
-
-
1
def option_name
-
OptionName
-
end
-
-
1
def objects_count
-
documents.count
-
end
-
-
1
def transfer_relations(to)
-
documents.update_all("category_id = #{to.id}")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentCategoryCustomField < CustomField
-
1
def type_name
-
:enumeration_doc_categories
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class DocumentObserver < ActiveRecord::Observer
-
1
def after_create(document)
-
Mailer.document_added(document).deliver if Setting.notified_events.include?('document_added')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class EnabledModule < ActiveRecord::Base
-
1
belongs_to :project
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => :project_id
-
-
1
after_create :module_enabled
-
-
1
private
-
-
# after_create callback used to do things when a module is enabled
-
1
def module_enabled
-
698
case name
-
when 'wiki'
-
# Create a wiki with a default start page
-
46
if project && project.wiki.nil?
-
46
Wiki.create(:project => project, :start_page => 'Wiki')
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Enumeration < ActiveRecord::Base
-
1
include Redmine::SubclassFactory
-
-
1
default_scope :order => "#{Enumeration.table_name}.position ASC"
-
-
1
belongs_to :project
-
-
1
acts_as_list :scope => 'type = \'#{type}\''
-
1
acts_as_customizable
-
1
acts_as_tree :order => 'position ASC'
-
-
1
before_destroy :check_integrity
-
1
before_save :check_default
-
-
1
attr_protected :type
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => [:type, :project_id]
-
1
validates_length_of :name, :maximum => 30
-
-
1
scope :shared, :conditions => { :project_id => nil }
-
1
scope :active, :conditions => { :active => true }
-
1
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
-
-
1
def self.default
-
# Creates a fake default scope so Enumeration.default will check
-
# it's type. STI subclasses will automatically add their own
-
# types to the finder.
-
5338
if self.descends_from_active_record?
-
find(:first, :conditions => { :is_default => true, :type => 'Enumeration' })
-
else
-
# STI classes are
-
5338
find(:first, :conditions => { :is_default => true })
-
end
-
end
-
-
# Overloaded on concrete classes
-
1
def option_name
-
nil
-
end
-
-
1
def check_default
-
if is_default? && is_default_changed?
-
Enumeration.update_all("is_default = #{connection.quoted_false}", {:type => type})
-
end
-
end
-
-
# Overloaded on concrete classes
-
1
def objects_count
-
0
-
end
-
-
1
def in_use?
-
self.objects_count != 0
-
end
-
-
# Is this enumeration overiding a system level enumeration?
-
1
def is_override?
-
!self.parent.nil?
-
end
-
-
1
alias :destroy_without_reassign :destroy
-
-
# Destroy the enumeration
-
# If a enumeration is specified, objects are reassigned
-
1
def destroy(reassign_to = nil)
-
if reassign_to && reassign_to.is_a?(Enumeration)
-
self.transfer_relations(reassign_to)
-
end
-
destroy_without_reassign
-
end
-
-
1
def <=>(enumeration)
-
position <=> enumeration.position
-
end
-
-
2311
def to_s; name end
-
-
# Returns the Subclasses of Enumeration. Each Subclass needs to be
-
# required in development mode.
-
#
-
# Note: subclasses is protected in ActiveRecord
-
1
def self.get_subclasses
-
subclasses
-
end
-
-
# Does the +new+ Hash override the previous Enumeration?
-
1
def self.overridding_change?(new, previous)
-
if (same_active_state?(new['active'], previous.active)) && same_custom_values?(new,previous)
-
return false
-
else
-
return true
-
end
-
end
-
-
# Does the +new+ Hash have the same custom values as the previous Enumeration?
-
1
def self.same_custom_values?(new, previous)
-
previous.custom_field_values.each do |custom_value|
-
if custom_value.value != new["custom_field_values"][custom_value.custom_field_id.to_s]
-
return false
-
end
-
end
-
-
return true
-
end
-
-
# Are the new and previous fields equal?
-
1
def self.same_active_state?(new, previous)
-
new = (new == "1" ? true : false)
-
return new == previous
-
end
-
-
1
private
-
1
def check_integrity
-
raise "Can't delete enumeration" if self.in_use?
-
end
-
-
end
-
-
# Force load the subclasses in development mode
-
1
require_dependency 'time_entry_activity'
-
1
require_dependency 'document_category'
-
1
require_dependency 'issue_priority'
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Group < Principal
-
1
has_and_belongs_to_many :users, :after_add => :user_added,
-
:after_remove => :user_removed
-
-
1
acts_as_customizable
-
-
1
validates_presence_of :lastname
-
1
validates_uniqueness_of :lastname, :case_sensitive => false
-
1
validates_length_of :lastname, :maximum => 30
-
-
1
before_destroy :remove_references_before_destroy
-
-
1
def to_s
-
46
lastname.to_s
-
end
-
-
1
alias :name :to_s
-
-
1
def user_added(user)
-
members.each do |member|
-
next if member.project.nil?
-
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
-
member.member_roles.each do |member_role|
-
user_member.member_roles << MemberRole.new(:role => member_role.role, :inherited_from => member_role.id)
-
end
-
user_member.save!
-
end
-
end
-
-
1
def user_removed(user)
-
members.each do |member|
-
MemberRole.find(:all, :include => :member,
-
:conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
-
end
-
end
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == 'lastname'
-
attr_name = "name"
-
end
-
super(attr_name, *args)
-
end
-
-
1
private
-
-
# Removes references that are not handled by associations
-
1
def remove_references_before_destroy
-
return if self.id.nil?
-
-
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class GroupCustomField < CustomField
-
1
def type_name
-
:label_group_plural
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Issue < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
-
1
belongs_to :project
-
1
belongs_to :tracker
-
1
belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
-
1
belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
-
1
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
-
1
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
-
-
1
has_many :journals, :as => :journalized, :dependent => :destroy
-
1
has_many :time_entries, :dependent => :delete_all
-
1
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
-
-
1
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
-
1
has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
-
-
1
acts_as_nested_set :scope => 'root_id', :dependent => :destroy
-
1
acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
-
1
acts_as_customizable
-
1
acts_as_watchable
-
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
-
:include => [:project, :journals],
-
# sort by id so that limited eager loading doesn't break with postgresql
-
1
:order_column => "#{table_name}.id"
-
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
-
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
-
1
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
-
-
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
-
1
:author_key => :author_id
-
-
1
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
-
-
1
attr_reader :current_journal
-
-
1
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
-
-
1
validates_length_of :subject, :maximum => 255
-
1
validates_inclusion_of :done_ratio, :in => 0..100
-
1
validates_numericality_of :estimated_hours, :allow_nil => true
-
1
validate :validate_issue
-
-
1
scope :visible,
-
lambda {|*args| { :include => :project,
-
659
:conditions => Issue.visible_condition(args.shift || User.current, *args) } }
-
-
1
scope :open, lambda {|*args|
-
246
is_closed = args.size > 0 ? !args.first : false
-
246
{:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
-
}
-
-
1
scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
-
1
scope :with_limit, lambda { |limit| { :limit => limit} }
-
1
scope :on_active_project, :include => [:status, :project, :tracker],
-
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
-
-
1
before_create :default_assign
-
1
before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
-
1484
after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
-
1
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
-
1
after_destroy :update_parent_attributes
-
-
# Returns a SQL conditions string used to find all issues visible by the specified user
-
1
def self.visible_condition(user, options={})
-
659
Project.allowed_to_condition(user, :view_issues, options) do |role, user|
-
1977
case role.issues_visibility
-
when 'all'
-
659
nil
-
when 'default'
-
1318
user_ids = [user.id] + user.groups.map(&:id)
-
1318
"(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
-
when 'own'
-
user_ids = [user.id] + user.groups.map(&:id)
-
"(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
-
else
-
'1=0'
-
end
-
end
-
end
-
-
# Returns true if usr or current user is allowed to view the issue
-
1
def visible?(usr=nil)
-
1913
(usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
-
1900
case role.issues_visibility
-
when 'all'
-
1119
true
-
when 'default'
-
781
!self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
-
when 'own'
-
self.author == user || user.is_or_belongs_to?(assigned_to)
-
else
-
false
-
end
-
end
-
end
-
-
1
def initialize(attributes=nil, *args)
-
2722
super
-
2722
if new_record?
-
# set default values for new records only
-
2722
self.status ||= IssueStatus.default
-
2722
self.priority ||= IssuePriority.default
-
2722
self.watcher_user_ids = []
-
end
-
end
-
-
# Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
-
1
def available_custom_fields
-
1455
(project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
-
end
-
-
# Copies attributes from another issue, arg can be an id or an Issue
-
1
def copy_from(arg, options={})
-
6
issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
-
6
self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
-
6
self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
-
6
self.status = issue.status
-
6
self.author = User.current
-
6
unless options[:attachments] == false
-
4
self.attachments = issue.attachments.map do |attachement|
-
attachement.copy(:container => self)
-
end
-
end
-
6
@copied_from = issue
-
6
self
-
end
-
-
# Returns an unsaved copy of the issue
-
1
def copy(attributes=nil, copy_options={})
-
copy = self.class.new.copy_from(self, copy_options)
-
copy.attributes = attributes if attributes
-
copy
-
end
-
-
# Returns true if the issue is a copy
-
1
def copy?
-
240
@copied_from.present?
-
end
-
-
# Moves/copies an issue to a new project and tracker
-
# Returns the moved/copied issue on success, false on failure
-
1
def move_to_project(new_project, new_tracker=nil, options={})
-
ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
-
-
if options[:copy]
-
issue = self.copy
-
else
-
issue = self
-
end
-
-
issue.init_journal(User.current, options[:notes])
-
-
# Preserve previous behaviour
-
# #move_to_project doesn't change tracker automatically
-
issue.send :project=, new_project, true
-
if new_tracker
-
issue.tracker = new_tracker
-
end
-
# Allow bulk setting of attributes on the issue
-
if options[:attributes]
-
issue.attributes = options[:attributes]
-
end
-
-
issue.save ? issue : false
-
end
-
-
1
def status_id=(sid)
-
1067
self.status = nil
-
1067
write_attribute(:status_id, sid)
-
end
-
-
1
def priority_id=(pid)
-
1009
self.priority = nil
-
1009
write_attribute(:priority_id, pid)
-
end
-
-
1
def category_id=(cid)
-
1009
self.category = nil
-
1009
write_attribute(:category_id, cid)
-
end
-
-
1
def fixed_version_id=(vid)
-
1013
self.fixed_version = nil
-
1013
write_attribute(:fixed_version_id, vid)
-
end
-
-
1
def tracker_id=(tid)
-
1013
self.tracker = nil
-
1013
result = write_attribute(:tracker_id, tid)
-
1013
@custom_field_values = nil
-
1013
result
-
end
-
-
1
def project_id=(project_id)
-
1236
if project_id.to_s != self.project_id.to_s
-
1234
self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
-
end
-
end
-
-
1
def project=(project, keep_tracker=false)
-
1242
project_was = self.project
-
1242
write_attribute(:project_id, project ? project.id : nil)
-
1242
association_instance_set('project', project)
-
1242
if project_was && project && project_was != project
-
unless keep_tracker || project.trackers.include?(tracker)
-
self.tracker = project.trackers.first
-
end
-
# Reassign to the category with same name if any
-
if category
-
self.category = project.issue_categories.find_by_name(category.name)
-
end
-
# Keep the fixed_version if it's still valid in the new_project
-
if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
-
self.fixed_version = nil
-
end
-
if parent && parent.project_id != project_id
-
self.parent_issue_id = nil
-
end
-
@custom_field_values = nil
-
end
-
end
-
-
1
def description=(arg)
-
1009
if arg.is_a?(String)
-
2
arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
-
end
-
1009
write_attribute(:description, arg)
-
end
-
-
# Overrides assign_attributes so that project and tracker get assigned first
-
1
def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
-
1274
return if new_attributes.nil?
-
1274
attrs = new_attributes.dup
-
1274
attrs.stringify_keys!
-
-
1274
%w(project project_id tracker tracker_id).each do |attr|
-
5096
if attrs.has_key?(attr)
-
2245
send "#{attr}=", attrs.delete(attr)
-
end
-
end
-
1274
send :assign_attributes_without_project_and_tracker_first, attrs, *args
-
end
-
# Do not redefine alias chain on reload (see #4838)
-
1
alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
-
-
1
def estimated_hours=(h)
-
1543
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
-
end
-
-
1
safe_attributes 'project_id',
-
:if => lambda {|issue, user|
-
242
if issue.new_record?
-
240
issue.copy?
-
elsif user.allowed_to?(:move_issues, issue.project)
-
2
projects = Issue.allowed_target_projects_on_move(user)
-
2
projects.include?(issue.project) && projects.size > 1
-
end
-
}
-
-
1
safe_attributes 'tracker_id',
-
'status_id',
-
'category_id',
-
'assigned_to_id',
-
'priority_id',
-
'fixed_version_id',
-
'subject',
-
'description',
-
'start_date',
-
'due_date',
-
'done_ratio',
-
'estimated_hours',
-
'custom_field_values',
-
'custom_fields',
-
'lock_version',
-
242
:if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
-
-
1
safe_attributes 'status_id',
-
'assigned_to_id',
-
'fixed_version_id',
-
'done_ratio',
-
'lock_version',
-
242
:if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
-
-
1
safe_attributes 'watcher_user_ids',
-
242
:if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
-
-
1
safe_attributes 'is_private',
-
:if => lambda {|issue, user|
-
user.allowed_to?(:set_issues_private, issue.project) ||
-
242
(issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
-
}
-
-
1
safe_attributes 'parent_issue_id',
-
242
:if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
-
242
user.allowed_to?(:manage_subtasks, issue.project)}
-
-
# Safely sets attributes
-
# Should be called from controllers instead of #attributes=
-
# attr_accessible is too rough because we still want things like
-
# Issue.new(:project => foo) to work
-
1
def safe_attributes=(attrs, user=User.current)
-
4
return unless attrs.is_a?(Hash)
-
-
# User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
-
2
attrs = delete_unsafe_attributes(attrs, user)
-
2
return if attrs.empty?
-
-
# Project and Tracker must be set before since new_statuses_allowed_to depends on it.
-
2
if p = attrs.delete('project_id')
-
2
if allowed_target_projects(user).collect(&:id).include?(p.to_i)
-
2
self.project_id = p
-
end
-
end
-
-
2
if t = attrs.delete('tracker_id')
-
2
self.tracker_id = t
-
end
-
-
2
if attrs['status_id']
-
2
unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
-
attrs.delete('status_id')
-
end
-
end
-
-
2
unless leaf?
-
attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
-
end
-
-
2
if attrs['parent_issue_id'].present?
-
attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
-
end
-
-
# mass-assignment security bypass
-
2
assign_attributes attrs, :without_protection => true
-
end
-
-
1
def done_ratio
-
1375
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
-
status.default_done_ratio
-
else
-
1375
read_attribute(:done_ratio)
-
end
-
end
-
-
1
def self.use_status_for_done_ratio?
-
3145
Setting.issue_done_ratio == 'issue_status'
-
end
-
-
1
def self.use_field_for_done_ratio?
-
3
Setting.issue_done_ratio == 'issue_field'
-
end
-
-
1
def validate_issue
-
1195
if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
-
errors.add :due_date, :not_a_date
-
end
-
-
1195
if self.due_date and self.start_date and self.due_date < self.start_date
-
errors.add :due_date, :greater_than_start_date
-
end
-
-
1195
if start_date && soonest_start && start_date < soonest_start
-
errors.add :start_date, :invalid
-
end
-
-
1195
if fixed_version
-
665
if !assignable_versions.include?(fixed_version)
-
errors.add :fixed_version_id, :inclusion
-
elsif reopened? && fixed_version.closed?
-
errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
-
end
-
end
-
-
# Checks that the issue can not be added/moved to a disabled tracker
-
1195
if project && (tracker_id_changed? || project_id_changed?)
-
996
unless project.trackers.include?(tracker)
-
errors.add :tracker_id, :inclusion
-
end
-
end
-
-
# Checks parent issue assignment
-
1195
if @parent_issue
-
182
if @parent_issue.project_id != project_id
-
errors.add :parent_issue_id, :not_same_project
-
182
elsif !new_record?
-
# moving an existing issue
-
60
if @parent_issue.root_id != root_id
-
# we can always move to another tree
-
52
elsif move_possible?(@parent_issue)
-
# move accepted inside tree
-
else
-
errors.add :parent_issue_id, :not_a_valid_parent
-
end
-
end
-
end
-
end
-
-
# Set the done_ratio using the status if that setting is set. This will keep the done_ratios
-
# even if the user turns off the setting later
-
1
def update_done_ratio_from_issue_status
-
1483
if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
-
self.done_ratio = status.default_done_ratio
-
end
-
end
-
-
1
def init_journal(user, notes = "")
-
281
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
-
281
if new_record?
-
@current_journal.notify = false
-
else
-
281
@attributes_before_change = attributes.dup
-
281
@custom_values_before_change = {}
-
281
self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
-
end
-
281
@current_journal
-
end
-
-
# Returns the id of the last journal or nil
-
1
def last_journal_id
-
2
if new_record?
-
nil
-
else
-
2
journals.first(:order => "#{Journal.table_name}.id DESC").try(:id)
-
end
-
end
-
-
# Return true if the issue is closed, otherwise false
-
1
def closed?
-
741
self.status.is_closed?
-
end
-
-
# Return true if the issue is being reopened
-
1
def reopened?
-
665
if !new_record? && status_id_changed?
-
54
status_was = IssueStatus.find_by_id(status_id_was)
-
54
status_new = IssueStatus.find_by_id(status_id)
-
54
if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
-
return true
-
end
-
end
-
665
false
-
end
-
-
# Return true if the issue is being closed
-
1
def closing?
-
1483
if !new_record? && status_id_changed?
-
55
status_was = IssueStatus.find_by_id(status_id_was)
-
55
status_new = IssueStatus.find_by_id(status_id)
-
55
if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
-
27
return true
-
end
-
end
-
1456
false
-
end
-
-
# Returns true if the issue is overdue
-
1
def overdue?
-
564
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
-
end
-
-
# Is the amount of work done less than it should for the due date
-
1
def behind_schedule?
-
return false if start_date.nil? || due_date.nil?
-
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
-
return done_date <= Date.today
-
end
-
-
# Does this issue have children?
-
1
def children?
-
!leaf?
-
end
-
-
# Users the issue can be assigned to
-
1
def assignable_users
-
4
users = project.assignable_users
-
4
users << author if author
-
4
users << assigned_to if assigned_to
-
4
users.uniq.sort
-
end
-
-
# Versions that the issue can be assigned to
-
1
def assignable_versions
-
673
@assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
-
end
-
-
# Returns true if this issue is blocked by another issue that is still open
-
1
def blocked?
-
244
!relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
-
end
-
-
# Returns an array of statuses that user is able to apply
-
1
def new_statuses_allowed_to(user=User.current, include_default=false)
-
250
if new_record? && @copied_from
-
10
[IssueStatus.default, @copied_from.status].compact.uniq.sort
-
else
-
240
initial_status = nil
-
240
if new_record?
-
236
initial_status = IssueStatus.default
-
elsif status_id_was
-
4
initial_status = IssueStatus.find_by_id(status_id_was)
-
end
-
240
initial_status ||= status
-
-
240
statuses = initial_status.find_new_statuses_allowed_to(
-
user.admin ? Role.all : user.roles_for_project(project),
-
tracker,
-
author == user,
-
assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
-
)
-
240
statuses << initial_status unless statuses.empty?
-
240
statuses << IssueStatus.default if include_default
-
240
statuses = statuses.compact.uniq.sort
-
240
blocked? ? statuses.reject {|s| s.is_closed?} : statuses
-
end
-
end
-
-
1
def assigned_to_was
-
1112
if assigned_to_id_changed? && assigned_to_id_was.present?
-
@assigned_to_was ||= User.find_by_id(assigned_to_id_was)
-
end
-
end
-
-
# Returns the mail adresses of users that should be notified
-
1
def recipients
-
1112
notified = []
-
# Author and assignee are always notified unless they have been
-
# locked or don't want to be notified
-
1112
notified << author if author
-
1112
if assigned_to
-
notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
-
end
-
1112
if assigned_to_was
-
notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
-
end
-
2224
notified = notified.select {|u| u.active? && u.notify_about?(self)}
-
-
1112
notified += project.notified_users
-
1112
notified.uniq!
-
# Remove users that can not view the issue
-
3018
notified.reject! {|user| !visible?(user)}
-
1112
notified.collect(&:mail)
-
end
-
-
# Returns the number of hours spent on this issue
-
1
def spent_hours
-
752
@spent_hours ||= time_entries.sum(:hours) || 0
-
end
-
-
# Returns the total number of hours spent on this issue and its descendants
-
#
-
# Example:
-
# spent_hours => 0.0
-
# spent_hours => 50.2
-
1
def total_spent_hours
-
@total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
-
2
:joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
-
end
-
-
1
def relations
-
10
@relations ||= (relations_from + relations_to).sort
-
end
-
-
# Preloads relations for a collection of issues
-
1
def self.load_relations(issues)
-
if issues.any?
-
relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
-
issues.each do |issue|
-
issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
-
end
-
end
-
end
-
-
# Preloads visible spent time for a collection of issues
-
1
def self.load_visible_spent_hours(issues, user=User.current)
-
if issues.any?
-
hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
-
issues.each do |issue|
-
issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
-
end
-
end
-
end
-
-
# Finds an issue relation given its id.
-
1
def find_relation(relation_id)
-
IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
-
end
-
-
1
def all_dependent_issues(except=[])
-
108
except << self
-
108
dependencies = []
-
108
relations_from.each do |relation|
-
if relation.issue_to && !except.include?(relation.issue_to)
-
dependencies << relation.issue_to
-
dependencies += relation.issue_to.all_dependent_issues(except)
-
end
-
end
-
108
dependencies
-
end
-
-
# Returns an array of issues that duplicate this one
-
1
def duplicates
-
27
relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
-
end
-
-
# Returns the due date or the target due date if any
-
# Used on gantt chart
-
1
def due_before
-
due_date || (fixed_version ? fixed_version.effective_date : nil)
-
end
-
-
# Returns the time scheduled for this issue.
-
#
-
# Example:
-
# Start Date: 2/26/09, End Date: 3/04/09
-
# duration => 6
-
1
def duration
-
(start_date && due_date) ? due_date - start_date : 0
-
end
-
-
1
def soonest_start
-
@soonest_start ||= (
-
7
relations_to.collect{|relation| relation.successor_soonest_start} +
-
454
ancestors.collect(&:soonest_start)
-
454
).compact.max
-
end
-
-
1
def reschedule_after(date)
-
return if date.nil?
-
if leaf?
-
if start_date.nil? || start_date < date
-
self.start_date, self.due_date = date, date + duration
-
begin
-
save
-
rescue ActiveRecord::StaleObjectError
-
reload
-
self.start_date, self.due_date = date, date + duration
-
save
-
end
-
end
-
else
-
leaves.each do |leaf|
-
leaf.reschedule_after(date)
-
end
-
end
-
end
-
-
1
def <=>(issue)
-
if issue.nil?
-
-1
-
elsif root_id != issue.root_id
-
(root_id || 0) <=> (issue.root_id || 0)
-
else
-
(lft || 0) <=> (issue.lft || 0)
-
end
-
end
-
-
1
def to_s
-
"#{tracker} ##{id}: #{subject}"
-
end
-
-
# Returns a string of css classes that apply to the issue
-
1
def css_classes
-
564
s = "issue status-#{status.position} priority-#{priority.position}"
-
564
s << ' closed' if closed?
-
564
s << ' overdue' if overdue?
-
564
s << ' child' if child?
-
564
s << ' parent' unless leaf?
-
564
s << ' private' if is_private?
-
564
s << ' created-by-me' if User.current.logged? && author_id == User.current.id
-
564
s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
-
564
s
-
end
-
-
# Saves an issue and a time_entry from the parameters
-
1
def save_issue_with_child_records(params, existing_time_entry=nil)
-
Issue.transaction do
-
if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
-
@time_entry = existing_time_entry || TimeEntry.new
-
@time_entry.project = project
-
@time_entry.issue = self
-
@time_entry.user = User.current
-
@time_entry.spent_on = User.current.today
-
@time_entry.attributes = params[:time_entry]
-
self.time_entries << @time_entry
-
end
-
-
# TODO: Rename hook
-
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
-
if save
-
# TODO: Rename hook
-
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
-
else
-
raise ActiveRecord::Rollback
-
end
-
end
-
end
-
-
# Unassigns issues from +version+ if it's no longer shared with issue's project
-
1
def self.update_versions_from_sharing_change(version)
-
# Update issues assigned to the version
-
update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
-
end
-
-
# Unassigns issues from versions that are no longer shared
-
# after +project+ was moved
-
1
def self.update_versions_from_hierarchy_change(project)
-
28
moved_project_ids = project.self_and_descendants.reload.collect(&:id)
-
# Update issues of the moved projects and issues assigned to a version of a moved project
-
28
Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
-
end
-
-
1
def parent_issue_id=(arg)
-
184
parent_issue_id = arg.blank? ? nil : arg.to_i
-
184
if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
-
182
@parent_issue.id
-
else
-
2
@parent_issue = nil
-
nil
-
end
-
end
-
-
1
def parent_issue_id
-
1531
if instance_variable_defined? :@parent_issue
-
184
@parent_issue.nil? ? nil : @parent_issue.id
-
else
-
1347
parent_id
-
end
-
end
-
-
# Extracted from the ReportsController.
-
1
def self.by_tracker(project)
-
count_and_group_by(:project => project,
-
:field => 'tracker_id',
-
:joins => Tracker.table_name)
-
end
-
-
1
def self.by_version(project)
-
count_and_group_by(:project => project,
-
:field => 'fixed_version_id',
-
:joins => Version.table_name)
-
end
-
-
1
def self.by_priority(project)
-
count_and_group_by(:project => project,
-
:field => 'priority_id',
-
:joins => IssuePriority.table_name)
-
end
-
-
1
def self.by_category(project)
-
count_and_group_by(:project => project,
-
:field => 'category_id',
-
:joins => IssueCategory.table_name)
-
end
-
-
1
def self.by_assigned_to(project)
-
count_and_group_by(:project => project,
-
:field => 'assigned_to_id',
-
:joins => User.table_name)
-
end
-
-
1
def self.by_author(project)
-
count_and_group_by(:project => project,
-
:field => 'author_id',
-
:joins => User.table_name)
-
end
-
-
1
def self.by_subproject(project)
-
ActiveRecord::Base.connection.select_all("select s.id as status_id,
-
s.is_closed as closed,
-
#{Issue.table_name}.project_id as project_id,
-
count(#{Issue.table_name}.id) as total
-
from
-
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
-
where
-
#{Issue.table_name}.status_id=s.id
-
and #{Issue.table_name}.project_id = #{Project.table_name}.id
-
and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
-
and #{Issue.table_name}.project_id <> #{project.id}
-
group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
-
end
-
# End ReportsController extraction
-
-
# Returns an array of projects that user can assign the issue to
-
1
def allowed_target_projects(user=User.current)
-
6
if new_record?
-
4
Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
-
else
-
2
self.class.allowed_target_projects_on_move(user)
-
end
-
end
-
-
# Returns an array of projects that user can move issues to
-
1
def self.allowed_target_projects_on_move(user=User.current)
-
4
Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
-
end
-
-
1
private
-
-
1
def after_project_change
-
# Update project_id on related time entries
-
TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
-
-
# Delete issue relations
-
unless Setting.cross_project_issue_relations?
-
relations_from.clear
-
relations_to.clear
-
end
-
-
# Move subtasks
-
children.each do |child|
-
# Change project and keep project
-
child.send :project=, project, true
-
unless child.save
-
raise ActiveRecord::Rollback
-
end
-
end
-
end
-
-
1
def update_nested_set_attributes
-
1483
if root_id.nil?
-
# issue was just created
-
996
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
-
996
set_default_left_and_right
-
996
Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
-
996
if @parent_issue
-
122
move_to_child_of(@parent_issue)
-
end
-
996
reload
-
elsif parent_issue_id != parent_id
-
8
former_parent_id = parent_id
-
# moving an existing issue
-
8
if @parent_issue && @parent_issue.root_id == root_id
-
# inside the same tree
-
move_to_child_of(@parent_issue)
-
else
-
# to another tree
-
8
unless root?
-
move_to_right_of(root)
-
reload
-
end
-
8
old_root_id = root_id
-
8
self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
-
8
target_maxright = nested_set_scope.maximum(right_column_name) || 0
-
8
offset = target_maxright + 1 - lft
-
8
Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
-
["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
-
8
self[left_column_name] = lft + offset
-
8
self[right_column_name] = rgt + offset
-
8
if @parent_issue
-
8
move_to_child_of(@parent_issue)
-
end
-
end
-
8
reload
-
# delete invalid relations of all descendants
-
8
self_and_descendants.each do |issue|
-
8
issue.relations.each do |relation|
-
relation.destroy unless relation.valid?
-
end
-
end
-
# update former parent
-
8
recalculate_attributes_for(former_parent_id) if former_parent_id
-
end
-
1483
remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
-
end
-
-
1
def update_parent_attributes
-
2785
recalculate_attributes_for(parent_id) if parent_id
-
end
-
-
1
def recalculate_attributes_for(issue_id)
-
287
if issue_id && p = Issue.find_by_id(issue_id)
-
# priority = highest priority of children
-
287
if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
-
287
p.priority = IssuePriority.find_by_position(priority_position)
-
end
-
-
# start/due dates = lowest/highest dates of children
-
287
p.start_date = p.children.minimum(:start_date)
-
287
p.due_date = p.children.maximum(:due_date)
-
287
if p.start_date && p.due_date && p.due_date < p.start_date
-
p.start_date, p.due_date = p.due_date, p.start_date
-
end
-
-
# done ratio = weighted average ratio of leaves
-
287
unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
-
287
leaves_count = p.leaves.count
-
287
if leaves_count > 0
-
287
average = p.leaves.average(:estimated_hours).to_f
-
287
if average == 0
-
70
average = 1
-
end
-
287
done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
-
287
progress = done / (average * leaves_count)
-
287
p.done_ratio = progress.round
-
end
-
end
-
-
# estimate = sum of leaves estimates
-
287
p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
-
287
p.estimated_hours = nil if p.estimated_hours == 0.0
-
-
# ancestors will be recursively updated
-
287
p.save(:validate => false)
-
end
-
end
-
-
# Update issues so their versions are not pointing to a
-
# fixed_version that is not shared with the issue's project
-
1
def self.update_versions(conditions=nil)
-
# Only need to update issues with a fixed_version from
-
# a different project and that is not systemwide shared
-
28
Issue.scoped(:conditions => conditions).all(
-
:conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
-
" AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
-
" AND #{Version.table_name}.sharing <> 'system'",
-
:include => [:project, :fixed_version]
-
).each do |issue|
-
next if issue.project.nil? || issue.fixed_version.nil?
-
unless issue.project.shared_versions.include?(issue.fixed_version)
-
issue.init_journal(User.current)
-
issue.fixed_version = nil
-
issue.save
-
end
-
end
-
end
-
-
# Callback on attachment deletion
-
1
def attachment_added(obj)
-
if @current_journal && !obj.new_record?
-
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
-
end
-
end
-
-
# Callback on attachment deletion
-
1
def attachment_removed(obj)
-
1116
if @current_journal && !obj.new_record?
-
@current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
-
@current_journal.save
-
end
-
end
-
-
# Default assignment based on category
-
1
def default_assign
-
996
if assigned_to.nil? && category && category.assigned_to
-
self.assigned_to = category.assigned_to
-
end
-
end
-
-
# Updates start/due dates of following issues
-
1
def reschedule_following_issues
-
1483
if start_date_changed? || due_date_changed?
-
530
relations_from.each do |relation|
-
relation.set_issue_to_dates
-
end
-
end
-
end
-
-
# Closes duplicates if the issue is being closed
-
1
def close_duplicates
-
1483
if closing?
-
27
duplicates.each do |duplicate|
-
# Reload is need in case the duplicate was updated by a previous duplicate
-
duplicate.reload
-
# Don't re-close it if it's already closed
-
next if duplicate.closed?
-
# Same user and notes
-
if @current_journal
-
duplicate.init_journal(@current_journal.user, @current_journal.notes)
-
end
-
duplicate.update_attribute :status, self.status
-
end
-
end
-
end
-
-
# Make sure updated_on is updated when adding a note
-
1
def force_updated_on_change
-
1483
if @current_journal
-
142
self.updated_on = current_time_from_proper_timezone
-
end
-
end
-
-
# Saves the changes in a Journal
-
# Called after_save
-
1
def create_journal
-
1483
if @current_journal
-
# attributes changes
-
142
if @attributes_before_change
-
142
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
-
2698
before = @attributes_before_change[c]
-
2698
after = send(c)
-
2698
next if before == after || (before.blank? && after.blank?)
-
@current_journal.details << JournalDetail.new(:property => 'attr',
-
:prop_key => c,
-
:old_value => before,
-
153
:value => after)
-
}
-
end
-
142
if @custom_values_before_change
-
# custom fields changes
-
142
custom_field_values.each {|c|
-
before = @custom_values_before_change[c.custom_field_id]
-
after = c.value
-
next if before == after || (before.blank? && after.blank?)
-
-
if before.is_a?(Array) || after.is_a?(Array)
-
before = [before] unless before.is_a?(Array)
-
after = [after] unless after.is_a?(Array)
-
-
# values removed
-
(before - after).reject(&:blank?).each do |value|
-
@current_journal.details << JournalDetail.new(:property => 'cf',
-
:prop_key => c.custom_field_id,
-
:old_value => value,
-
:value => nil)
-
end
-
# values added
-
(after - before).reject(&:blank?).each do |value|
-
@current_journal.details << JournalDetail.new(:property => 'cf',
-
:prop_key => c.custom_field_id,
-
:old_value => nil,
-
:value => value)
-
end
-
else
-
@current_journal.details << JournalDetail.new(:property => 'cf',
-
:prop_key => c.custom_field_id,
-
:old_value => before,
-
:value => after)
-
end
-
}
-
end
-
142
@current_journal.save
-
# reset current journal
-
142
init_journal @current_journal.user, @current_journal.notes
-
end
-
end
-
-
# Query generator for selecting groups of issue counts for a project
-
# based on specific criteria
-
#
-
# Options
-
# * project - Project to search in.
-
# * field - String. Issue field to key off of in the grouping.
-
# * joins - String. The table name to join against.
-
1
def self.count_and_group_by(options)
-
project = options.delete(:project)
-
select_field = options.delete(:field)
-
joins = options.delete(:joins)
-
-
where = "#{Issue.table_name}.#{select_field}=j.id"
-
-
ActiveRecord::Base.connection.select_all("select s.id as status_id,
-
s.is_closed as closed,
-
j.id as #{select_field},
-
count(#{Issue.table_name}.id) as total
-
from
-
#{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
-
where
-
#{Issue.table_name}.status_id=s.id
-
and #{where}
-
and #{Issue.table_name}.project_id=#{Project.table_name}.id
-
and #{visible_condition(User.current, :project => project)}
-
group by s.id, s.is_closed, j.id")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueCategory < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
-
1
has_many :issues, :foreign_key => 'category_id', :dependent => :nullify
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => [:project_id]
-
1
validates_length_of :name, :maximum => 30
-
-
1
safe_attributes 'name', 'assigned_to_id'
-
-
1
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
-
-
1
alias :destroy_without_reassign :destroy
-
-
# Destroy the category
-
# If a category is specified, issues are reassigned to this category
-
1
def destroy(reassign_to = nil)
-
if reassign_to && reassign_to.is_a?(IssueCategory) && reassign_to.project == self.project
-
Issue.update_all("category_id = #{reassign_to.id}", "category_id = #{id}")
-
end
-
destroy_without_reassign
-
end
-
-
1
def <=>(category)
-
name <=> category.name
-
end
-
-
1
def to_s; name end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueCustomField < CustomField
-
1
has_and_belongs_to_many :projects, :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :foreign_key => "custom_field_id"
-
1
has_and_belongs_to_many :trackers, :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :foreign_key => "custom_field_id"
-
1
has_many :issues, :through => :issue_custom_values
-
-
1
def type_name
-
:label_issue_plural
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueObserver < ActiveRecord::Observer
-
1
def after_create(issue)
-
996
Mailer.issue_add(issue).deliver if Setting.notified_events.include?('issue_added')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssuePriority < Enumeration
-
1
has_many :issues, :foreign_key => 'priority_id'
-
-
1
OptionName = :enumeration_issue_priorities
-
-
1
def option_name
-
OptionName
-
end
-
-
1
def objects_count
-
issues.count
-
end
-
-
1
def transfer_relations(to)
-
issues.update_all("priority_id = #{to.id}")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssuePriorityCustomField < CustomField
-
1
def type_name
-
:enumeration_issue_priorities
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueRelation < ActiveRecord::Base
-
1
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
-
1
belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
-
-
1
TYPE_RELATES = "relates"
-
1
TYPE_DUPLICATES = "duplicates"
-
1
TYPE_DUPLICATED = "duplicated"
-
1
TYPE_BLOCKS = "blocks"
-
1
TYPE_BLOCKED = "blocked"
-
1
TYPE_PRECEDES = "precedes"
-
1
TYPE_FOLLOWS = "follows"
-
-
1
TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
-
TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
-
TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
-
TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED },
-
TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
-
TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
-
TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }
-
}.freeze
-
-
1
validates_presence_of :issue_from, :issue_to, :relation_type
-
1
validates_inclusion_of :relation_type, :in => TYPES.keys
-
1
validates_numericality_of :delay, :allow_nil => true
-
1
validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
-
-
1
validate :validate_issue_relation
-
-
1
attr_protected :issue_from_id, :issue_to_id
-
-
1
before_save :handle_issue_order
-
-
1
def visible?(user=User.current)
-
(issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
-
end
-
-
1
def deletable?(user=User.current)
-
visible?(user) &&
-
((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
-
(issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
-
end
-
-
1
def initialize(attributes=nil, *args)
-
108
super
-
108
if new_record?
-
108
if relation_type.blank?
-
2
self.relation_type = IssueRelation::TYPE_RELATES
-
end
-
end
-
end
-
-
1
def validate_issue_relation
-
108
if issue_from && issue_to
-
108
errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
-
108
errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
-
#detect circular dependencies depending wether the relation should be reversed
-
108
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
-
errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
-
else
-
108
errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
-
end
-
108
errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
-
end
-
end
-
-
1
def other_issue(issue)
-
14
(self.issue_from_id == issue.id) ? issue_to : issue_from
-
end
-
-
# Returns the relation type for +issue+
-
1
def relation_type_for(issue)
-
if TYPES[relation_type]
-
if self.issue_from_id == issue.id
-
relation_type
-
else
-
TYPES[relation_type][:sym]
-
end
-
end
-
end
-
-
1
def label_for(issue)
-
2
TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
-
end
-
-
1
def handle_issue_order
-
107
reverse_if_needed
-
-
107
if TYPE_PRECEDES == relation_type
-
self.delay ||= 0
-
else
-
107
self.delay = nil
-
end
-
107
set_issue_to_dates
-
end
-
-
1
def set_issue_to_dates
-
107
soonest_start = self.successor_soonest_start
-
107
if soonest_start && issue_to
-
issue_to.reschedule_after(soonest_start)
-
end
-
end
-
-
1
def successor_soonest_start
-
114
if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
-
(issue_from.due_date || issue_from.start_date) + 1 + delay
-
end
-
end
-
-
1
def <=>(relation)
-
TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
-
end
-
-
1
private
-
-
# Reverses the relation if needed so that it gets stored in the proper way
-
# Should not be reversed before validation so that it can be displayed back
-
# as entered on new relation form
-
1
def reverse_if_needed
-
107
if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
-
issue_tmp = issue_to
-
self.issue_to = issue_from
-
self.issue_from = issue_tmp
-
self.relation_type = TYPES[relation_type][:reverse]
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class IssueStatus < ActiveRecord::Base
-
1
before_destroy :check_integrity
-
1
has_many :workflows, :foreign_key => "old_status_id"
-
1
acts_as_list
-
-
1
before_destroy :delete_workflows
-
1
after_save :update_default
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 30
-
1
validates_inclusion_of :default_done_ratio, :in => 0..100, :allow_nil => true
-
-
1
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
-
-
1
def update_default
-
112
IssueStatus.update_all("is_default=#{connection.quoted_false}", ['id <> ?', id]) if self.is_default?
-
end
-
-
# Returns the default status for new issues
-
1
def self.default
-
2554
find(:first, :conditions =>["is_default=?", true])
-
end
-
-
# Update all the +Issues+ setting their done_ratio to the value of their +IssueStatus+
-
1
def self.update_issue_done_ratios
-
if Issue.use_status_for_done_ratio?
-
IssueStatus.find(:all, :conditions => ["default_done_ratio >= 0"]).each do |status|
-
Issue.update_all(["done_ratio = ?", status.default_done_ratio],
-
["status_id = ?", status.id])
-
end
-
end
-
-
return Issue.use_status_for_done_ratio?
-
end
-
-
# Returns an array of all statuses the given role can switch to
-
# Uses association cache when called more than one time
-
1
def new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
-
1944
if roles && tracker
-
1944
role_ids = roles.collect(&:id)
-
1944
transitions = workflows.select do |w|
-
role_ids.include?(w.role_id) &&
-
145152
w.tracker_id == tracker.id &&
-
6360
((!w.author && !w.assignee) || (author && w.author) || (assignee && w.assignee))
-
end
-
8304
transitions.collect{|w| w.new_status}.compact.sort
-
else
-
[]
-
end
-
end
-
-
# Same thing as above but uses a database query
-
# More efficient than the previous method if called just once
-
1
def find_new_statuses_allowed_to(roles, tracker, author=false, assignee=false)
-
240
if roles.present? && tracker
-
4
conditions = "(author = :false AND assignee = :false)"
-
4
conditions << " OR author = :true" if author
-
4
conditions << " OR assignee = :true" if assignee
-
-
4
workflows.find(:all,
-
:include => :new_status,
-
:conditions => ["role_id IN (:role_ids) AND tracker_id = :tracker_id AND (#{conditions})",
-
{:role_ids => roles.collect(&:id), :tracker_id => tracker.id, :true => true, :false => false}
-
]
-
20
).collect{|w| w.new_status}.compact.sort
-
else
-
236
[]
-
end
-
end
-
-
1
def <=>(status)
-
8348
position <=> status.position
-
end
-
-
2782
def to_s; name end
-
-
1
private
-
1
def check_integrity
-
raise "Can't delete status" if Issue.find(:first, :conditions => ["status_id=?", self.id])
-
end
-
-
# Deletes associated workflows
-
1
def delete_workflows
-
Workflow.delete_all(["old_status_id = :id OR new_status_id = :id", {:id => id}])
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Journal < ActiveRecord::Base
-
1
belongs_to :journalized, :polymorphic => true
-
# added as a quick fix to allow eager loading of the polymorphic association
-
# since always associated to an issue, for now
-
1
belongs_to :issue, :foreign_key => :journalized_id
-
-
1
belongs_to :user
-
1
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
-
1
attr_accessor :indice
-
-
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
-
:description => :notes,
-
:author => :user,
-
:type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
-
1
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
-
-
acts_as_activity_provider :type => 'issues',
-
:author_key => :user_id,
-
:find_options => {:include => [{:issue => :project}, :details, :user],
-
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
-
1
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
-
-
1
scope :visible, lambda {|*args| {
-
:include => {:issue => :project},
-
:conditions => Issue.visible_condition(args.shift || User.current, *args)
-
}}
-
-
1
def save(*args)
-
# Do not save an empty journal
-
142
(details.empty? && notes.blank?) ? false : super
-
end
-
-
# Returns the new status if the journal contains a status change, otherwise nil
-
1
def new_status
-
c = details.detect {|detail| detail.prop_key == 'status_id'}
-
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
-
end
-
-
1
def new_value_for(prop)
-
245
c = details.detect {|detail| detail.prop_key == prop}
-
116
c ? c.value : nil
-
end
-
-
1
def editable_by?(usr)
-
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
-
end
-
-
1
def project
-
journalized.respond_to?(:project) ? journalized.project : nil
-
end
-
-
1
def attachments
-
journalized.respond_to?(:attachments) ? journalized.attachments : nil
-
end
-
-
# Returns a string of css classes
-
1
def css_classes
-
s = 'journal'
-
s << ' has-notes' unless notes.blank?
-
s << ' has-details' unless details.blank?
-
s
-
end
-
-
1
def notify?
-
116
@notify != false
-
end
-
-
1
def notify=(arg)
-
@notify = arg
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class JournalDetail < ActiveRecord::Base
-
1
belongs_to :journal
-
1
before_save :normalize_values
-
-
1
private
-
-
1
def normalize_values
-
161
self.value = normalize(value)
-
161
self.old_value = normalize(old_value)
-
end
-
-
1
def normalize(v)
-
322
if v == true
-
"1"
-
322
elsif v == false
-
"0"
-
else
-
322
v
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class JournalObserver < ActiveRecord::Observer
-
1
def after_create(journal)
-
if journal.notify? &&
-
(Setting.notified_events.include?('issue_updated') ||
-
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
-
(Setting.notified_events.include?('issue_status_updated') && journal.new_status.present?) ||
-
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?)
-
116
)
-
116
Mailer.issue_edit(journal).deliver
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MailHandler < ActionMailer::Base
-
1
include ActionView::Helpers::SanitizeHelper
-
1
include Redmine::I18n
-
-
1
class UnauthorizedAction < StandardError; end
-
1
class MissingInformation < StandardError; end
-
-
1
attr_reader :email, :user
-
-
1
def self.receive(email, options={})
-
@@handler_options = options.dup
-
-
@@handler_options[:issue] ||= {}
-
-
if @@handler_options[:allow_override].is_a?(String)
-
@@handler_options[:allow_override] = @@handler_options[:allow_override].split(',').collect(&:strip)
-
end
-
@@handler_options[:allow_override] ||= []
-
# Project needs to be overridable if not specified
-
@@handler_options[:allow_override] << 'project' unless @@handler_options[:issue].has_key?(:project)
-
# Status overridable by default
-
@@handler_options[:allow_override] << 'status' unless @@handler_options[:issue].has_key?(:status)
-
-
@@handler_options[:no_permission_check] = (@@handler_options[:no_permission_check].to_s == '1' ? true : false)
-
-
email.force_encoding('ASCII-8BIT') if email.respond_to?(:force_encoding)
-
super(email)
-
end
-
-
1
def logger
-
Rails.logger
-
end
-
-
1
cattr_accessor :ignored_emails_headers
-
1
@@ignored_emails_headers = {
-
'X-Auto-Response-Suppress' => 'OOF',
-
'Auto-Submitted' => 'auto-replied'
-
}
-
-
# Processes incoming emails
-
# Returns the created object (eg. an issue, a message) or false
-
1
def receive(email)
-
@email = email
-
sender_email = email.from.to_a.first.to_s.strip
-
# Ignore emails received from the application emission address to avoid hell cycles
-
if sender_email.downcase == Setting.mail_from.to_s.strip.downcase
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email from Redmine emission address [#{sender_email}]"
-
end
-
return false
-
end
-
# Ignore auto generated emails
-
self.class.ignored_emails_headers.each do |key, ignored_value|
-
value = email.header[key]
-
if value && value.to_s.downcase == ignored_value.downcase
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email with #{key}:#{value} header"
-
end
-
return false
-
end
-
end
-
@user = User.find_by_mail(sender_email) if sender_email.present?
-
if @user && !@user.active?
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email from non-active user [#{@user.login}]"
-
end
-
return false
-
end
-
if @user.nil?
-
# Email was submitted by an unknown user
-
case @@handler_options[:unknown_user]
-
when 'accept'
-
@user = User.anonymous
-
when 'create'
-
@user = create_user_from_email
-
if @user
-
if logger && logger.info
-
logger.info "MailHandler: [#{@user.login}] account created"
-
end
-
Mailer.account_information(@user, @user.password).deliver
-
else
-
if logger && logger.error
-
logger.error "MailHandler: could not create account for [#{sender_email}]"
-
end
-
return false
-
end
-
else
-
# Default behaviour, emails from unknown users are ignored
-
if logger && logger.info
-
logger.info "MailHandler: ignoring email from unknown user [#{sender_email}]"
-
end
-
return false
-
end
-
end
-
User.current = @user
-
dispatch
-
end
-
-
1
private
-
-
1
MESSAGE_ID_RE = %r{^<?redmine\.([a-z0-9_]+)\-(\d+)\.\d+@}
-
1
ISSUE_REPLY_SUBJECT_RE = %r{\[[^\]]*#(\d+)\]}
-
1
MESSAGE_REPLY_SUBJECT_RE = %r{\[[^\]]*msg(\d+)\]}
-
-
1
def dispatch
-
headers = [email.in_reply_to, email.references].flatten.compact
-
if headers.detect {|h| h.to_s =~ MESSAGE_ID_RE}
-
klass, object_id = $1, $2.to_i
-
method_name = "receive_#{klass}_reply"
-
if self.class.private_instance_methods.collect(&:to_s).include?(method_name)
-
send method_name, object_id
-
else
-
# ignoring it
-
end
-
elsif m = email.subject.match(ISSUE_REPLY_SUBJECT_RE)
-
receive_issue_reply(m[1].to_i)
-
elsif m = email.subject.match(MESSAGE_REPLY_SUBJECT_RE)
-
receive_message_reply(m[1].to_i)
-
else
-
dispatch_to_default
-
end
-
rescue ActiveRecord::RecordInvalid => e
-
# TODO: send a email to the user
-
logger.error e.message if logger
-
false
-
rescue MissingInformation => e
-
logger.error "MailHandler: missing information from #{user}: #{e.message}" if logger
-
false
-
rescue UnauthorizedAction => e
-
logger.error "MailHandler: unauthorized attempt from #{user}" if logger
-
false
-
end
-
-
1
def dispatch_to_default
-
receive_issue
-
end
-
-
# Creates a new issue
-
1
def receive_issue
-
project = target_project
-
# check permission
-
unless @@handler_options[:no_permission_check]
-
raise UnauthorizedAction unless user.allowed_to?(:add_issues, project)
-
end
-
-
issue = Issue.new(:author => user, :project => project)
-
issue.safe_attributes = issue_attributes_from_keywords(issue)
-
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
-
issue.subject = email.subject.to_s.chomp[0,255]
-
if issue.subject.blank?
-
issue.subject = '(no subject)'
-
end
-
issue.description = cleaned_up_text_body
-
-
# add To and Cc as watchers before saving so the watchers can reply to Redmine
-
add_watchers(issue)
-
issue.save!
-
add_attachments(issue)
-
logger.info "MailHandler: issue ##{issue.id} created by #{user}" if logger && logger.info
-
issue
-
end
-
-
# Adds a note to an existing issue
-
1
def receive_issue_reply(issue_id)
-
issue = Issue.find_by_id(issue_id)
-
return unless issue
-
# check permission
-
unless @@handler_options[:no_permission_check]
-
unless user.allowed_to?(:add_issue_notes, issue.project) ||
-
user.allowed_to?(:edit_issues, issue.project)
-
raise UnauthorizedAction
-
end
-
end
-
-
# ignore CLI-supplied defaults for new issues
-
@@handler_options[:issue].clear
-
-
journal = issue.init_journal(user)
-
issue.safe_attributes = issue_attributes_from_keywords(issue)
-
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
-
journal.notes = cleaned_up_text_body
-
add_attachments(issue)
-
issue.save!
-
if logger && logger.info
-
logger.info "MailHandler: issue ##{issue.id} updated by #{user}"
-
end
-
journal
-
end
-
-
# Reply will be added to the issue
-
1
def receive_journal_reply(journal_id)
-
journal = Journal.find_by_id(journal_id)
-
if journal && journal.journalized_type == 'Issue'
-
receive_issue_reply(journal.journalized_id)
-
end
-
end
-
-
# Receives a reply to a forum message
-
1
def receive_message_reply(message_id)
-
message = Message.find_by_id(message_id)
-
if message
-
message = message.root
-
-
unless @@handler_options[:no_permission_check]
-
raise UnauthorizedAction unless user.allowed_to?(:add_messages, message.project)
-
end
-
-
if !message.locked?
-
reply = Message.new(:subject => email.subject.gsub(%r{^.*msg\d+\]}, '').strip,
-
:content => cleaned_up_text_body)
-
reply.author = user
-
reply.board = message.board
-
message.children << reply
-
add_attachments(reply)
-
reply
-
else
-
if logger && logger.info
-
logger.info "MailHandler: ignoring reply from [#{sender_email}] to a locked topic"
-
end
-
end
-
end
-
end
-
-
1
def add_attachments(obj)
-
if email.attachments && email.attachments.any?
-
email.attachments.each do |attachment|
-
obj.attachments << Attachment.create(:container => obj,
-
:file => attachment.decoded,
-
:filename => attachment.filename,
-
:author => user,
-
:content_type => attachment.mime_type)
-
end
-
end
-
end
-
-
# Adds To and Cc as watchers of the given object if the sender has the
-
# appropriate permission
-
1
def add_watchers(obj)
-
if user.allowed_to?("add_#{obj.class.name.underscore}_watchers".to_sym, obj.project)
-
addresses = [email.to, email.cc].flatten.compact.uniq.collect {|a| a.strip.downcase}
-
unless addresses.empty?
-
watchers = User.active.find(:all, :conditions => ['LOWER(mail) IN (?)', addresses])
-
watchers.each {|w| obj.add_watcher(w)}
-
end
-
end
-
end
-
-
1
def get_keyword(attr, options={})
-
@keywords ||= {}
-
if @keywords.has_key?(attr)
-
@keywords[attr]
-
else
-
@keywords[attr] = begin
-
if (options[:override] || @@handler_options[:allow_override].include?(attr.to_s)) &&
-
(v = extract_keyword!(plain_text_body, attr, options[:format]))
-
v
-
elsif !@@handler_options[:issue][attr].blank?
-
@@handler_options[:issue][attr]
-
end
-
end
-
end
-
end
-
-
# Destructively extracts the value for +attr+ in +text+
-
# Returns nil if no matching keyword found
-
1
def extract_keyword!(text, attr, format=nil)
-
keys = [attr.to_s.humanize]
-
if attr.is_a?(Symbol)
-
if user && user.language.present?
-
keys << l("field_#{attr}", :default => '', :locale => user.language)
-
end
-
if Setting.default_language.present?
-
keys << l("field_#{attr}", :default => '', :locale => Setting.default_language)
-
end
-
end
-
keys.reject! {|k| k.blank?}
-
keys.collect! {|k| Regexp.escape(k)}
-
format ||= '.+'
-
keyword = nil
-
regexp = /^(#{keys.join('|')})[ \t]*:[ \t]*(#{format})\s*$/i
-
if m = text.match(regexp)
-
keyword = m[2].strip
-
text.gsub!(regexp, '')
-
end
-
keyword
-
end
-
-
1
def target_project
-
# TODO: other ways to specify project:
-
# * parse the email To field
-
# * specific project (eg. Setting.mail_handler_target_project)
-
target = Project.find_by_identifier(get_keyword(:project))
-
raise MissingInformation.new('Unable to determine target project') if target.nil?
-
target
-
end
-
-
# Returns a Hash of issue attributes extracted from keywords in the email body
-
1
def issue_attributes_from_keywords(issue)
-
assigned_to = (k = get_keyword(:assigned_to, :override => true)) && find_assignee_from_keyword(k, issue)
-
-
attrs = {
-
'tracker_id' => (k = get_keyword(:tracker)) && issue.project.trackers.named(k).first.try(:id),
-
'status_id' => (k = get_keyword(:status)) && IssueStatus.named(k).first.try(:id),
-
'priority_id' => (k = get_keyword(:priority)) && IssuePriority.named(k).first.try(:id),
-
'category_id' => (k = get_keyword(:category)) && issue.project.issue_categories.named(k).first.try(:id),
-
'assigned_to_id' => assigned_to.try(:id),
-
'fixed_version_id' => (k = get_keyword(:fixed_version, :override => true)) &&
-
issue.project.shared_versions.named(k).first.try(:id),
-
'start_date' => get_keyword(:start_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
-
'due_date' => get_keyword(:due_date, :override => true, :format => '\d{4}-\d{2}-\d{2}'),
-
'estimated_hours' => get_keyword(:estimated_hours, :override => true),
-
'done_ratio' => get_keyword(:done_ratio, :override => true, :format => '(\d|10)?0')
-
}.delete_if {|k, v| v.blank? }
-
-
if issue.new_record? && attrs['tracker_id'].nil?
-
attrs['tracker_id'] = issue.project.trackers.find(:first).try(:id)
-
end
-
-
attrs
-
end
-
-
# Returns a Hash of issue custom field values extracted from keywords in the email body
-
1
def custom_field_values_from_keywords(customized)
-
customized.custom_field_values.inject({}) do |h, v|
-
if value = get_keyword(v.custom_field.name, :override => true)
-
h[v.custom_field.id.to_s] = value
-
end
-
h
-
end
-
end
-
-
# Returns the text/plain part of the email
-
# If not found (eg. HTML-only email), returns the body with tags removed
-
1
def plain_text_body
-
return @plain_text_body unless @plain_text_body.nil?
-
-
part = email.text_part || email.html_part || email
-
@plain_text_body = Redmine::CodesetUtil.to_utf8(part.body.decoded, part.charset)
-
-
# strip html tags and remove doctype directive
-
@plain_text_body = strip_tags(@plain_text_body.strip)
-
@plain_text_body.sub! %r{^<!DOCTYPE .*$}, ''
-
@plain_text_body
-
end
-
-
1
def cleaned_up_text_body
-
cleanup_body(plain_text_body)
-
end
-
-
1
def self.full_sanitizer
-
@full_sanitizer ||= HTML::FullSanitizer.new
-
end
-
-
1
def self.assign_string_attribute_with_limit(object, attribute, value, limit=nil)
-
limit ||= object.class.columns_hash[attribute.to_s].limit || 255
-
value = value.to_s.slice(0, limit)
-
object.send("#{attribute}=", value)
-
end
-
-
# Returns a User from an email address and a full name
-
1
def self.new_user_from_attributes(email_address, fullname=nil)
-
user = User.new
-
-
# Truncating the email address would result in an invalid format
-
user.mail = email_address
-
assign_string_attribute_with_limit(user, 'login', email_address, User::LOGIN_LENGTH_LIMIT)
-
-
names = fullname.blank? ? email_address.gsub(/@.*$/, '').split('.') : fullname.split
-
assign_string_attribute_with_limit(user, 'firstname', names.shift)
-
assign_string_attribute_with_limit(user, 'lastname', names.join(' '))
-
user.lastname = '-' if user.lastname.blank?
-
-
password_length = [Setting.password_min_length.to_i, 10].max
-
user.password = Redmine::Utils.random_hex(password_length / 2 + 1)
-
user.language = Setting.default_language
-
-
unless user.valid?
-
user.login = "user#{Redmine::Utils.random_hex(6)}" unless user.errors[:login].blank?
-
user.firstname = "-" unless user.errors[:firstname].blank?
-
user.lastname = "-" unless user.errors[:lastname].blank?
-
end
-
-
user
-
end
-
-
# Creates a User for the +email+ sender
-
# Returns the user or nil if it could not be created
-
1
def create_user_from_email
-
from = email.header['from'].to_s
-
addr, name = from, nil
-
if m = from.match(/^"?(.+?)"?\s+<(.+@.+)>$/)
-
addr, name = m[2], m[1]
-
end
-
if addr.present?
-
user = self.class.new_user_from_attributes(addr, name)
-
if user.save
-
user
-
else
-
logger.error "MailHandler: failed to create User: #{user.errors.full_messages}" if logger
-
nil
-
end
-
else
-
logger.error "MailHandler: failed to create User: no FROM address found" if logger
-
nil
-
end
-
end
-
-
# Removes the email body of text after the truncation configurations.
-
1
def cleanup_body(body)
-
delimiters = Setting.mail_handler_body_delimiters.to_s.split(/[\r\n]+/).reject(&:blank?).map {|s| Regexp.escape(s)}
-
unless delimiters.empty?
-
regex = Regexp.new("^[> ]*(#{ delimiters.join('|') })\s*[\r\n].*", Regexp::MULTILINE)
-
body = body.gsub(regex, '')
-
end
-
body.strip
-
end
-
-
1
def find_assignee_from_keyword(keyword, issue)
-
keyword = keyword.to_s.downcase
-
assignable = issue.assignable_users
-
assignee = nil
-
assignee ||= assignable.detect {|a|
-
a.mail.to_s.downcase == keyword ||
-
a.login.to_s.downcase == keyword
-
}
-
if assignee.nil? && keyword.match(/ /)
-
firstname, lastname = *(keyword.split) # "First Last Throwaway"
-
assignee ||= assignable.detect {|a|
-
a.is_a?(User) && a.firstname.to_s.downcase == firstname &&
-
a.lastname.to_s.downcase == lastname
-
}
-
end
-
if assignee.nil?
-
assignee ||= assignable.detect {|a| a.is_a?(Group) && a.name.downcase == keyword}
-
end
-
assignee
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Mailer < ActionMailer::Base
-
1
layout 'mailer'
-
1
helper :application
-
1
helper :issues
-
1
helper :custom_fields
-
-
1
include Redmine::I18n
-
-
1
def self.default_url_options
-
1112
{ :host => Setting.host_name, :protocol => Setting.protocol }
-
end
-
-
# Builds a Mail::Message object used to email recipients of the added issue.
-
#
-
# Example:
-
# issue_add(issue) => Mail::Message object
-
# Mailer.issue_add(issue).deliver => sends an email to issue recipients
-
1
def issue_add(issue)
-
redmine_headers 'Project' => issue.project.identifier,
-
'Issue-Id' => issue.id,
-
996
'Issue-Author' => issue.author.login
-
996
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
-
996
message_id issue
-
996
@author = issue.author
-
996
@issue = issue
-
996
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue)
-
996
recipients = issue.recipients
-
996
cc = issue.watcher_recipients - recipients
-
mail :to => recipients,
-
:cc => cc,
-
996
:subject => "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of the edited issue.
-
#
-
# Example:
-
# issue_edit(journal) => Mail::Message object
-
# Mailer.issue_edit(journal).deliver => sends an email to issue recipients
-
1
def issue_edit(journal)
-
116
issue = journal.journalized.reload
-
redmine_headers 'Project' => issue.project.identifier,
-
'Issue-Id' => issue.id,
-
116
'Issue-Author' => issue.author.login
-
116
redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
-
116
message_id journal
-
116
references issue
-
116
@author = journal.user
-
116
recipients = issue.recipients
-
# Watchers in cc
-
116
cc = issue.watcher_recipients - recipients
-
116
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
-
116
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
-
116
s << issue.subject
-
116
@issue = issue
-
116
@journal = journal
-
116
@issue_url = url_for(:controller => 'issues', :action => 'show', :id => issue, :anchor => "change-#{journal.id}")
-
mail :to => recipients,
-
:cc => cc,
-
116
:subject => s
-
end
-
-
1
def reminder(user, issues, days)
-
set_language_if_valid user.language
-
@issues = issues
-
@days = days
-
@issues_url = url_for(:controller => 'issues', :action => 'index',
-
:set_filter => 1, :assigned_to_id => user.id,
-
:sort => 'due_date:asc')
-
mail :to => user.mail,
-
:subject => l(:mail_subject_reminder, :count => issues.size, :days => days)
-
end
-
-
# Builds a Mail::Message object used to email users belonging to the added document's project.
-
#
-
# Example:
-
# document_added(document) => Mail::Message object
-
# Mailer.document_added(document).deliver => sends an email to the document's project recipients
-
1
def document_added(document)
-
redmine_headers 'Project' => document.project.identifier
-
@author = User.current
-
@document = document
-
@document_url = url_for(:controller => 'documents', :action => 'show', :id => document)
-
mail :to => document.recipients,
-
:subject => "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of a project when an attachements are added.
-
#
-
# Example:
-
# attachments_added(attachments) => Mail::Message object
-
# Mailer.attachments_added(attachments).deliver => sends an email to the project's recipients
-
1
def attachments_added(attachments)
-
container = attachments.first.container
-
added_to = ''
-
added_to_url = ''
-
@author = attachments.first.author
-
case container.class.name
-
when 'Project'
-
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container)
-
added_to = "#{l(:label_project)}: #{container}"
-
recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
-
when 'Version'
-
added_to_url = url_for(:controller => 'files', :action => 'index', :project_id => container.project)
-
added_to = "#{l(:label_version)}: #{container.name}"
-
recipients = container.project.notified_users.select {|user| user.allowed_to?(:view_files, container.project)}.collect {|u| u.mail}
-
when 'Document'
-
added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
-
added_to = "#{l(:label_document)}: #{container.title}"
-
recipients = container.recipients
-
end
-
redmine_headers 'Project' => container.project.identifier
-
@attachments = attachments
-
@added_to = added_to
-
@added_to_url = added_to_url
-
mail :to => recipients,
-
:subject => "[#{container.project.name}] #{l(:label_attachment_new)}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of a news' project when a news item is added.
-
#
-
# Example:
-
# news_added(news) => Mail::Message object
-
# Mailer.news_added(news).deliver => sends an email to the news' project recipients
-
1
def news_added(news)
-
redmine_headers 'Project' => news.project.identifier
-
@author = news.author
-
message_id news
-
@news = news
-
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
-
mail :to => news.recipients,
-
:subject => "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
-
end
-
-
# Builds a Mail::Message object used to email recipients of a news' project when a news comment is added.
-
#
-
# Example:
-
# news_comment_added(comment) => Mail::Message object
-
# Mailer.news_comment_added(comment) => sends an email to the news' project recipients
-
1
def news_comment_added(comment)
-
news = comment.commented
-
redmine_headers 'Project' => news.project.identifier
-
@author = comment.author
-
message_id comment
-
@news = news
-
@comment = comment
-
@news_url = url_for(:controller => 'news', :action => 'show', :id => news)
-
mail :to => news.recipients,
-
:cc => news.watcher_recipients,
-
:subject => "Re: [#{news.project.name}] #{l(:label_news)}: #{news.title}"
-
end
-
-
# Builds a Mail::Message object used to email the recipients of the specified message that was posted.
-
#
-
# Example:
-
# message_posted(message) => Mail::Message object
-
# Mailer.message_posted(message).deliver => sends an email to the recipients
-
1
def message_posted(message)
-
redmine_headers 'Project' => message.project.identifier,
-
'Topic-Id' => (message.parent_id || message.id)
-
@author = message.author
-
message_id message
-
references message.parent unless message.parent.nil?
-
recipients = message.recipients
-
cc = ((message.root.watcher_recipients + message.board.watcher_recipients).uniq - recipients)
-
@message = message
-
@message_url = url_for(message.event_url)
-
mail :to => recipients,
-
:cc => cc,
-
:subject => "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
-
end
-
-
# Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was added.
-
#
-
# Example:
-
# wiki_content_added(wiki_content) => Mail::Message object
-
# Mailer.wiki_content_added(wiki_content).deliver => sends an email to the project's recipients
-
1
def wiki_content_added(wiki_content)
-
redmine_headers 'Project' => wiki_content.project.identifier,
-
'Wiki-Page-Id' => wiki_content.page.id
-
@author = wiki_content.author
-
message_id wiki_content
-
recipients = wiki_content.recipients
-
cc = wiki_content.page.wiki.watcher_recipients - recipients
-
@wiki_content = wiki_content
-
@wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
-
:project_id => wiki_content.project,
-
:id => wiki_content.page.title)
-
mail :to => recipients,
-
:cc => cc,
-
:subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :id => wiki_content.page.pretty_title)}"
-
end
-
-
# Builds a Mail::Message object used to email the recipients of a project of the specified wiki content was updated.
-
#
-
# Example:
-
# wiki_content_updated(wiki_content) => Mail::Message object
-
# Mailer.wiki_content_updated(wiki_content).deliver => sends an email to the project's recipients
-
1
def wiki_content_updated(wiki_content)
-
redmine_headers 'Project' => wiki_content.project.identifier,
-
'Wiki-Page-Id' => wiki_content.page.id
-
@author = wiki_content.author
-
message_id wiki_content
-
recipients = wiki_content.recipients
-
cc = wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients
-
@wiki_content = wiki_content
-
@wiki_content_url = url_for(:controller => 'wiki', :action => 'show',
-
:project_id => wiki_content.project,
-
:id => wiki_content.page.title)
-
@wiki_diff_url = url_for(:controller => 'wiki', :action => 'diff',
-
:project_id => wiki_content.project, :id => wiki_content.page.title,
-
:version => wiki_content.version)
-
mail :to => recipients,
-
:cc => cc,
-
:subject => "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :id => wiki_content.page.pretty_title)}"
-
end
-
-
# Builds a Mail::Message object used to email the specified user their account information.
-
#
-
# Example:
-
# account_information(user, password) => Mail::Message object
-
# Mailer.account_information(user, password).deliver => sends account information to the user
-
1
def account_information(user, password)
-
set_language_if_valid user.language
-
@user = user
-
@password = password
-
@login_url = url_for(:controller => 'account', :action => 'login')
-
mail :to => user.mail,
-
:subject => l(:mail_subject_register, Setting.app_title)
-
end
-
-
# Builds a Mail::Message object used to email all active administrators of an account activation request.
-
#
-
# Example:
-
# account_activation_request(user) => Mail::Message object
-
# Mailer.account_activation_request(user).deliver => sends an email to all active administrators
-
1
def account_activation_request(user)
-
# Send the email to all active administrators
-
recipients = User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
-
@user = user
-
@url = url_for(:controller => 'users', :action => 'index',
-
:status => User::STATUS_REGISTERED,
-
:sort_key => 'created_on', :sort_order => 'desc')
-
mail :to => recipients,
-
:subject => l(:mail_subject_account_activation_request, Setting.app_title)
-
end
-
-
# Builds a Mail::Message object used to email the specified user that their account was activated by an administrator.
-
#
-
# Example:
-
# account_activated(user) => Mail::Message object
-
# Mailer.account_activated(user).deliver => sends an email to the registered user
-
1
def account_activated(user)
-
set_language_if_valid user.language
-
@user = user
-
@login_url = url_for(:controller => 'account', :action => 'login')
-
mail :to => user.mail,
-
:subject => l(:mail_subject_register, Setting.app_title)
-
end
-
-
1
def lost_password(token)
-
set_language_if_valid(token.user.language)
-
@token = token
-
@url = url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
-
mail :to => token.user.mail,
-
:subject => l(:mail_subject_lost_password, Setting.app_title)
-
end
-
-
1
def register(token)
-
set_language_if_valid(token.user.language)
-
@token = token
-
@url = url_for(:controller => 'account', :action => 'activate', :token => token.value)
-
mail :to => token.user.mail,
-
:subject => l(:mail_subject_register, Setting.app_title)
-
end
-
-
1
def test_email(user)
-
set_language_if_valid(user.language)
-
@url = url_for(:controller => 'welcome')
-
mail :to => user.mail,
-
:subject => 'Redmine test'
-
end
-
-
# Overrides default deliver! method to prevent from sending an email
-
# with no recipient, cc or bcc
-
1
def deliver!(mail = @mail)
-
set_language_if_valid @initial_language
-
return false if (recipients.nil? || recipients.empty?) &&
-
(cc.nil? || cc.empty?) &&
-
(bcc.nil? || bcc.empty?)
-
-
-
# Log errors when raise_delivery_errors is set to false, Rails does not
-
raise_errors = self.class.raise_delivery_errors
-
self.class.raise_delivery_errors = true
-
begin
-
return super(mail)
-
rescue Exception => e
-
if raise_errors
-
raise e
-
elsif mylogger
-
mylogger.error "The following error occured while sending email notification: \"#{e.message}\". Check your configuration in config/configuration.yml."
-
end
-
ensure
-
self.class.raise_delivery_errors = raise_errors
-
end
-
end
-
-
# Sends reminders to issue assignees
-
# Available options:
-
# * :days => how many days in the future to remind about (defaults to 7)
-
# * :tracker => id of tracker for filtering issues (defaults to all trackers)
-
# * :project => id or identifier of project to process (defaults to all projects)
-
# * :users => array of user ids who should be reminded
-
1
def self.reminders(options={})
-
days = options[:days] || 7
-
project = options[:project] ? Project.find(options[:project]) : nil
-
tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
-
user_ids = options[:users]
-
-
scope = Issue.open.scoped(:conditions => ["#{Issue.table_name}.assigned_to_id IS NOT NULL" +
-
" AND #{Project.table_name}.status = #{Project::STATUS_ACTIVE}" +
-
" AND #{Issue.table_name}.due_date <= ?", days.day.from_now.to_date]
-
)
-
scope = scope.scoped(:conditions => {:assigned_to_id => user_ids}) if user_ids.present?
-
scope = scope.scoped(:conditions => {:project_id => project.id}) if project
-
scope = scope.scoped(:conditions => {:tracker_id => tracker.id}) if tracker
-
-
issues_by_assignee = scope.all(:include => [:status, :assigned_to, :project, :tracker]).group_by(&:assigned_to)
-
issues_by_assignee.each do |assignee, issues|
-
reminder(assignee, issues, days).deliver if assignee.is_a?(User) && assignee.active?
-
end
-
end
-
-
# Activates/desactivates email deliveries during +block+
-
1
def self.with_deliveries(enabled = true, &block)
-
was_enabled = ActionMailer::Base.perform_deliveries
-
ActionMailer::Base.perform_deliveries = !!enabled
-
yield
-
ensure
-
ActionMailer::Base.perform_deliveries = was_enabled
-
end
-
-
# Sends emails synchronously in the given block
-
1
def self.with_synched_deliveries(&block)
-
saved_method = ActionMailer::Base.delivery_method
-
if m = saved_method.to_s.match(%r{^async_(.+)$})
-
ActionMailer::Base.delivery_method = m[1].to_sym
-
end
-
yield
-
ensure
-
ActionMailer::Base.delivery_method = saved_method
-
end
-
-
1
def mail(headers={})
-
1112
headers.merge! 'X-Mailer' => 'Redmine',
-
'X-Redmine-Host' => Setting.host_name,
-
'X-Redmine-Site' => Setting.app_title,
-
'X-Auto-Response-Suppress' => 'OOF',
-
'Auto-Submitted' => 'auto-generated',
-
'From' => Setting.mail_from,
-
'List-Id' => "<#{Setting.mail_from.to_s.gsub('@', '.')}>"
-
-
# Removes the author from the recipients and cc
-
# if he doesn't want to receive notifications about what he does
-
1112
if @author && @author.logged? && @author.pref[:no_self_notified]
-
headers[:to].delete(@author.mail) if headers[:to].is_a?(Array)
-
headers[:cc].delete(@author.mail) if headers[:cc].is_a?(Array)
-
end
-
-
1112
if @author && @author.logged?
-
1112
redmine_headers 'Sender' => @author.login
-
end
-
-
# Blind carbon copy recipients
-
1112
if Setting.bcc_recipients?
-
1112
headers[:bcc] = [headers[:to], headers[:cc]].flatten.uniq.reject(&:blank?)
-
1112
headers[:to] = nil
-
1112
headers[:cc] = nil
-
end
-
-
1112
if @message_id_object
-
1112
headers[:message_id] = "<#{self.class.message_id_for(@message_id_object)}>"
-
end
-
1112
if @references_objects
-
232
headers[:references] = @references_objects.collect {|o| "<#{self.class.message_id_for(o)}>"}.join(' ')
-
end
-
-
1112
super headers do |format|
-
1112
format.text
-
1112
format.html unless Setting.plain_text_mail?
-
end
-
-
1112
set_language_if_valid @initial_language
-
end
-
-
1
def initialize(*args)
-
1112
@initial_language = current_language
-
1112
set_language_if_valid Setting.default_language
-
1112
super
-
end
-
-
1
def self.deliver_mail(mail)
-
1112
return false if mail.to.blank? && mail.cc.blank? && mail.bcc.blank?
-
1112
super
-
end
-
-
1
def self.method_missing(method, *args, &block)
-
1112
if m = method.to_s.match(%r{^deliver_(.+)$})
-
ActiveSupport::Deprecation.warn "Mailer.deliver_#{m[1]}(*args) is deprecated. Use Mailer.#{m[1]}(*args).deliver instead."
-
send(m[1], *args).deliver
-
else
-
1112
super
-
end
-
end
-
-
1
private
-
-
# Appends a Redmine header field (name is prepended with 'X-Redmine-')
-
1
def redmine_headers(h)
-
6672
h.each { |k,v| headers["X-Redmine-#{k}"] = v.to_s }
-
end
-
-
# Returns a predictable Message-Id for the given object
-
1
def self.message_id_for(object)
-
# id + timestamp should reduce the odds of a collision
-
# as far as we don't send multiple emails for the same object
-
1228
timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
-
1228
hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
-
1228
host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
-
1228
host = "#{::Socket.gethostname}.redmine" if host.empty?
-
1228
"#{hash}@#{host}"
-
end
-
-
1
def message_id(object)
-
1112
@message_id_object = object
-
end
-
-
1
def references(object)
-
116
@references_objects ||= []
-
116
@references_objects << object
-
end
-
-
1
def mylogger
-
Rails.logger
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Member < ActiveRecord::Base
-
1
belongs_to :user
-
1
belongs_to :principal, :foreign_key => 'user_id'
-
1
has_many :member_roles, :dependent => :destroy
-
1
has_many :roles, :through => :member_roles
-
1
belongs_to :project
-
-
1
validates_presence_of :principal, :project
-
1
validates_uniqueness_of :user_id, :scope => :project_id
-
1
validate :validate_role
-
-
1
before_destroy :set_issue_category_nil
-
1
after_destroy :unwatch_from_permission_change
-
-
1
def role
-
end
-
-
1
def role=
-
end
-
-
1
def name
-
self.user.name
-
end
-
-
1
alias :base_role_ids= :role_ids=
-
1
def role_ids=(arg)
-
ids = (arg || []).collect(&:to_i) - [0]
-
# Keep inherited roles
-
ids += member_roles.select {|mr| !mr.inherited_from.nil?}.collect(&:role_id)
-
-
new_role_ids = ids - role_ids
-
# Add new roles
-
new_role_ids.each {|id| member_roles << MemberRole.new(:role_id => id) }
-
# Remove roles (Rails' #role_ids= will not trigger MemberRole#on_destroy)
-
member_roles_to_destroy = member_roles.select {|mr| !ids.include?(mr.role_id)}
-
if member_roles_to_destroy.any?
-
member_roles_to_destroy.each(&:destroy)
-
unwatch_from_permission_change
-
end
-
end
-
-
1
def <=>(member)
-
a, b = roles.sort.first, member.roles.sort.first
-
if a == b
-
if principal
-
principal <=> member.principal
-
else
-
1
-
end
-
elsif a
-
a <=> b
-
else
-
1
-
end
-
end
-
-
1
def deletable?
-
member_roles.detect {|mr| mr.inherited_from}.nil?
-
end
-
-
1
def include?(user)
-
if principal.is_a?(Group)
-
!user.nil? && user.groups.include?(principal)
-
else
-
self.user == user
-
end
-
end
-
-
1
def set_issue_category_nil
-
if user
-
# remove category based auto assignments for this member
-
IssueCategory.update_all "assigned_to_id = NULL", ["project_id = ? AND assigned_to_id = ?", project.id, user.id]
-
end
-
end
-
-
# Find or initilize a Member with an id, attributes, and for a Principal
-
1
def self.edit_membership(id, new_attributes, principal=nil)
-
@membership = id.present? ? Member.find(id) : Member.new(:principal => principal)
-
@membership.attributes = new_attributes
-
@membership
-
end
-
-
1
protected
-
-
1
def validate_role
-
237
errors.add_on_empty :role if member_roles.empty? && roles.empty?
-
end
-
-
1
private
-
-
# Unwatch things that the user is no longer allowed to view inside project
-
1
def unwatch_from_permission_change
-
if user
-
Watcher.prune(:user => user, :project => project)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MemberRole < ActiveRecord::Base
-
1
belongs_to :member
-
1
belongs_to :role
-
-
1
after_destroy :remove_member_if_empty
-
-
1
after_create :add_role_to_group_users
-
1
after_destroy :remove_role_from_group_users
-
-
1
validates_presence_of :role
-
1
validate :validate_role_member
-
-
1
def validate_role_member
-
90
errors.add :role_id, :invalid if role && !role.member?
-
end
-
-
1
def inherited?
-
!inherited_from.nil?
-
end
-
-
1
private
-
-
1
def remove_member_if_empty
-
if member.roles.empty?
-
member.destroy
-
end
-
end
-
-
1
def add_role_to_group_users
-
90
if member.principal.is_a?(Group)
-
member.principal.users.each do |user|
-
user_member = Member.find_by_project_id_and_user_id(member.project_id, user.id) || Member.new(:project_id => member.project_id, :user_id => user.id)
-
user_member.member_roles << MemberRole.new(:role => role, :inherited_from => id)
-
user_member.save!
-
end
-
end
-
end
-
-
1
def remove_role_from_group_users
-
MemberRole.find(:all, :conditions => { :inherited_from => id }).group_by(&:member).each do |member, member_roles|
-
member_roles.each(&:destroy)
-
if member && member.user
-
Watcher.prune(:user => member.user, :project => member.project)
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Message < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :board
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
-
1
acts_as_attachable
-
1
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
-
-
acts_as_searchable :columns => ['subject', 'content'],
-
:include => {:board => :project},
-
:project_key => "#{Board.table_name}.project_id",
-
1
:date_column => "#{table_name}.created_on"
-
acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
-
:description => :content,
-
:type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
-
:url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
-
1
{:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
-
-
acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
-
1
:author_key => :author_id
-
1
acts_as_watchable
-
-
1
validates_presence_of :board, :subject, :content
-
1
validates_length_of :subject, :maximum => 255
-
1
validate :cannot_reply_to_locked_topic, :on => :create
-
-
1
after_create :add_author_as_watcher, :update_parent_last_reply
-
1
after_update :update_messages_board
-
1
after_destroy :reset_board_counters
-
-
1
scope :visible, lambda {|*args| { :include => {:board => :project},
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_messages, *args) } }
-
-
1
safe_attributes 'subject', 'content'
-
1
safe_attributes 'locked', 'sticky', 'board_id',
-
:if => lambda {|message, user|
-
user.allowed_to?(:edit_messages, message.project)
-
}
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_messages, project)
-
end
-
-
1
def cannot_reply_to_locked_topic
-
# Can not reply to a locked topic
-
errors.add :base, 'Topic is locked' if root.locked? && self != root
-
end
-
-
1
def update_parent_last_reply
-
if parent
-
parent.reload.update_attribute(:last_reply_id, self.id)
-
end
-
board.reset_counters!
-
end
-
-
1
def update_messages_board
-
if board_id_changed?
-
Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
-
Board.reset_counters!(board_id_was)
-
Board.reset_counters!(board_id)
-
end
-
end
-
-
1
def reset_board_counters
-
board.reset_counters!
-
end
-
-
1
def sticky=(arg)
-
write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
-
end
-
-
1
def sticky?
-
sticky == 1
-
end
-
-
1
def project
-
board.project
-
end
-
-
1
def editable_by?(usr)
-
usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
-
end
-
-
1
def destroyable_by?(usr)
-
usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
-
end
-
-
1
private
-
-
1
def add_author_as_watcher
-
Watcher.create(:watchable => self.root, :user => author)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class MessageObserver < ActiveRecord::Observer
-
1
def after_create(message)
-
Mailer.message_posted(message).deliver if Setting.notified_events.include?('message_posted')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class News < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on"
-
-
1
validates_presence_of :title, :description
-
1
validates_length_of :title, :maximum => 60
-
1
validates_length_of :summary, :maximum => 255
-
-
1
acts_as_attachable :delete_permission => :manage_news
-
1
acts_as_searchable :columns => ['title', 'summary', "#{table_name}.description"], :include => :project
-
1
acts_as_event :url => Proc.new {|o| {:controller => 'news', :action => 'show', :id => o.id}}
-
acts_as_activity_provider :find_options => {:include => [:project, :author]},
-
1
:author_key => :author_id
-
1
acts_as_watchable
-
-
1
after_create :add_author_as_watcher
-
-
1
scope :visible, lambda {|*args| {
-
:include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_news, *args)
-
4
}}
-
-
1
safe_attributes 'title', 'summary', 'description'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_news, project)
-
end
-
-
# Returns true if the news can be commented by user
-
1
def commentable?(user=User.current)
-
user.allowed_to?(:comment_news, project)
-
end
-
-
# returns latest news for projects visible by user
-
1
def self.latest(user = User.current, count = 5)
-
4
visible(user).includes([:author, :project]).order("#{News.table_name}.created_on DESC").limit(count).all
-
end
-
-
1
private
-
-
1
def add_author_as_watcher
-
Watcher.create(:watchable => self, :user => author)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class NewsObserver < ActiveRecord::Observer
-
1
def after_create(news)
-
Mailer.news_added(news).deliver if Setting.notified_events.include?('news_added')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Principal < ActiveRecord::Base
-
1
self.table_name = "#{table_name_prefix}users#{table_name_suffix}"
-
-
1
has_many :members, :foreign_key => 'user_id', :dependent => :destroy
-
1
has_many :memberships, :class_name => 'Member', :foreign_key => 'user_id', :include => [ :project, :roles ], :conditions => "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}", :order => "#{Project.table_name}.name"
-
1
has_many :projects, :through => :memberships
-
1
has_many :issue_categories, :foreign_key => 'assigned_to_id', :dependent => :nullify
-
-
# Groups and active users
-
1
scope :active, :conditions => "#{Principal.table_name}.status = 1"
-
-
1
scope :like, lambda {|q|
-
if q.blank?
-
{}
-
else
-
q = q.to_s.downcase
-
pattern = "%#{q}%"
-
sql = "LOWER(login) LIKE :p OR LOWER(firstname) LIKE :p OR LOWER(lastname) LIKE :p OR LOWER(mail) LIKE :p"
-
params = {:p => pattern}
-
if q =~ /^(.+)\s+(.+)$/
-
a, b = "#{$1}%", "#{$2}%"
-
sql << " OR (LOWER(firstname) LIKE :a AND LOWER(lastname) LIKE :b) OR (LOWER(firstname) LIKE :b AND LOWER(lastname) LIKE :a)"
-
params.merge!(:a => a, :b => b)
-
end
-
{:conditions => [sql, params]}
-
end
-
}
-
-
# Principals that are members of a collection of projects
-
1
scope :member_of, lambda {|projects|
-
23
projects = [projects] unless projects.is_a?(Array)
-
23
if projects.empty?
-
{:conditions => "1=0"}
-
else
-
23
ids = projects.map(&:id)
-
23
{:conditions => ["#{Principal.table_name}.status = 1 AND #{Principal.table_name}.id IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
-
end
-
}
-
# Principals that are not members of projects
-
1
scope :not_member_of, lambda {|projects|
-
projects = [projects] unless projects.is_a?(Array)
-
if projects.empty?
-
{:conditions => "1=0"}
-
else
-
ids = projects.map(&:id)
-
{:conditions => ["#{Principal.table_name}.id NOT IN (SELECT DISTINCT user_id FROM #{Member.table_name} WHERE project_id IN (?))", ids]}
-
end
-
}
-
-
1
before_create :set_default_empty_values
-
-
1
def name(formatter = nil)
-
to_s
-
end
-
-
1
def <=>(principal)
-
353
if principal.nil?
-
-1
-
353
elsif self.class.name == principal.class.name
-
307
self.to_s.downcase <=> principal.to_s.downcase
-
else
-
# groups after users
-
46
principal.class.name <=> self.class.name
-
end
-
end
-
-
1
protected
-
-
# Make sure we don't try to insert NULL values (see #4632)
-
1
def set_default_empty_values
-
self.login ||= ''
-
self.hashed_password ||= ''
-
self.firstname ||= ''
-
self.lastname ||= ''
-
self.mail ||= ''
-
true
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Project < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
-
# Project statuses
-
1
STATUS_ACTIVE = 1
-
1
STATUS_ARCHIVED = 9
-
-
# Maximum length for project identifiers
-
1
IDENTIFIER_MAX_LENGTH = 100
-
-
# Specific overidden Activities
-
1
has_many :time_entry_activities
-
1
has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
-
1
has_many :memberships, :class_name => 'Member'
-
1
has_many :member_principals, :class_name => 'Member',
-
:include => :principal,
-
:conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
-
1
has_many :users, :through => :members
-
1
has_many :principals, :through => :member_principals, :source => :principal
-
-
1
has_many :enabled_modules, :dependent => :delete_all
-
1
has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
-
1
has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
-
1
has_many :issue_changes, :through => :issues, :source => :journals
-
1
has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
-
1
has_many :time_entries, :dependent => :delete_all
-
1
has_many :queries, :dependent => :delete_all
-
1
has_many :documents, :dependent => :destroy
-
1
has_many :news, :dependent => :destroy, :include => :author
-
1
has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
-
1
has_many :boards, :dependent => :destroy, :order => "position ASC"
-
1
has_one :repository, :conditions => ["is_default = ?", true]
-
1
has_many :repositories, :dependent => :destroy
-
1
has_many :changesets, :through => :repository
-
1
has_one :wiki, :dependent => :destroy
-
# Custom field for the project issues
-
1
has_and_belongs_to_many :issue_custom_fields,
-
:class_name => 'IssueCustomField',
-
:order => "#{CustomField.table_name}.position",
-
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
-
:association_foreign_key => 'custom_field_id'
-
-
1
acts_as_nested_set :order => 'name', :dependent => :destroy
-
acts_as_attachable :view_permission => :view_files,
-
1
:delete_permission => :manage_files
-
-
1
acts_as_customizable
-
1
acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
-
acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
-
:url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
-
1
:author => nil
-
-
1
attr_protected :status
-
-
1
validates_presence_of :name, :identifier
-
1
validates_uniqueness_of :identifier
-
1
validates_associated :repository, :wiki
-
1
validates_length_of :name, :maximum => 255
-
1
validates_length_of :homepage, :maximum => 255
-
1
validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
-
# donwcase letters, digits, dashes but not digits only
-
47
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? }
-
# reserved words
-
1
validates_exclusion_of :identifier, :in => %w( new )
-
-
1
before_destroy :delete_all_members
-
-
1
scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
-
1
scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
-
1
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
-
1
scope :all_public, { :conditions => { :is_public => true } }
-
148
scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
-
1
scope :allowed_to, lambda {|*args|
-
user = User.current
-
permission = nil
-
if args.first.is_a?(Symbol)
-
permission = args.shift
-
else
-
user = args.shift
-
permission = args.shift
-
end
-
{ :conditions => Project.allowed_to_condition(user, permission, *args) }
-
}
-
1
scope :like, lambda {|arg|
-
if arg.blank?
-
{}
-
else
-
pattern = "%#{arg.to_s.strip.downcase}%"
-
{:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]}
-
end
-
}
-
-
1
def initialize(attributes=nil, *args)
-
46
super
-
-
46
initialized = (attributes || {}).stringify_keys
-
46
if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
-
self.identifier = Project.next_identifier
-
end
-
46
if !initialized.key?('is_public')
-
46
self.is_public = Setting.default_projects_public?
-
end
-
46
if !initialized.key?('enabled_module_names')
-
46
self.enabled_module_names = Setting.default_projects_modules
-
end
-
46
if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
-
46
self.trackers = Tracker.all
-
end
-
end
-
-
1
def identifier=(identifier)
-
46
super unless identifier_frozen?
-
end
-
-
1
def identifier_frozen?
-
46
errors[:identifier].blank? && !(new_record? || identifier.blank?)
-
end
-
-
# returns latest created projects
-
# non public projects will be returned only if user is a member of those
-
1
def self.latest(user=nil, count=5)
-
4
visible(user).find(:all, :limit => count, :order => "created_on DESC")
-
end
-
-
# Returns true if the project is visible to +user+ or to the current user.
-
1
def visible?(user=User.current)
-
user.allowed_to?(:view_project, self)
-
end
-
-
# Returns a SQL conditions string used to find all projects visible by the specified user.
-
#
-
# Examples:
-
# Project.visible_condition(admin) => "projects.status = 1"
-
# Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
-
# Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
-
1
def self.visible_condition(user, options={})
-
147
allowed_to_condition(user, :view_project, options)
-
end
-
-
# Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
-
#
-
# Valid options:
-
# * :project => limit the condition to project
-
# * :with_subprojects => limit the condition to project and its subprojects
-
# * :member => limit the condition to the user projects
-
1
def self.allowed_to_condition(user, permission, options={})
-
907
base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
-
907
if perm = Redmine::AccessControl.permission(permission)
-
907
unless perm.project_module.nil?
-
# If the permission belongs to a project module, make sure the module is enabled
-
760
base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
-
end
-
end
-
907
if options[:project]
-
project_statement = "#{Project.table_name}.id = #{options[:project].id}"
-
project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
-
base_statement = "(#{project_statement}) AND (#{base_statement})"
-
end
-
-
907
if user.admin?
-
base_statement
-
else
-
907
statement_by_role = {}
-
907
unless options[:member]
-
907
role = user.logged? ? Role.non_member : Role.anonymous
-
907
if role.allowed_to?(permission)
-
903
statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
-
end
-
end
-
907
if user.logged?
-
899
user.projects_by_role.each do |role, projects|
-
1798
if role.allowed_to?(permission)
-
1798
statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
-
end
-
end
-
end
-
907
if statement_by_role.empty?
-
"1=0"
-
else
-
907
if block_given?
-
659
statement_by_role.each do |role, statement|
-
1977
if s = yield(role, user)
-
1318
statement_by_role[role] = "(#{statement} AND (#{s}))"
-
end
-
end
-
end
-
907
"((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
-
end
-
end
-
end
-
-
# Returns the Systemwide and project specific activities
-
1
def activities(include_inactive=false)
-
4
if include_inactive
-
return all_activities
-
else
-
4
return active_activities
-
end
-
end
-
-
# Will create a new Project specific Activity or update an existing one
-
#
-
# This will raise a ActiveRecord::Rollback if the TimeEntryActivity
-
# does not successfully save.
-
1
def update_or_create_time_entry_activity(id, activity_hash)
-
if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
-
self.create_time_entry_activity_if_needed(activity_hash)
-
else
-
activity = project.time_entry_activities.find_by_id(id.to_i)
-
activity.update_attributes(activity_hash) if activity
-
end
-
end
-
-
# Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
-
#
-
# This will raise a ActiveRecord::Rollback if the TimeEntryActivity
-
# does not successfully save.
-
1
def create_time_entry_activity_if_needed(activity)
-
if activity['parent_id']
-
-
parent_activity = TimeEntryActivity.find(activity['parent_id'])
-
activity['name'] = parent_activity.name
-
activity['position'] = parent_activity.position
-
-
if Enumeration.overridding_change?(activity, parent_activity)
-
project_activity = self.time_entry_activities.create(activity)
-
-
if project_activity.new_record?
-
raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
-
else
-
self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
-
end
-
end
-
end
-
end
-
-
# Returns a :conditions SQL string that can be used to find the issues associated with this project.
-
#
-
# Examples:
-
# project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
-
# project.project_condition(false) => "projects.id = 1"
-
1
def project_condition(with_subprojects)
-
498
cond = "#{Project.table_name}.id = #{id}"
-
498
cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
-
498
cond
-
end
-
-
1
def self.find(*args)
-
2861
if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
-
1478
project = find_by_identifier(*args)
-
1478
raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
-
1478
project
-
else
-
1383
super
-
end
-
end
-
-
1
def self.find_by_param(*args)
-
self.find(*args)
-
end
-
-
1
def reload(*args)
-
112
@shared_versions = nil
-
112
@rolled_up_versions = nil
-
112
@rolled_up_trackers = nil
-
112
@all_issue_custom_fields = nil
-
112
@all_time_entry_custom_fields = nil
-
112
@to_param = nil
-
112
@allowed_parents = nil
-
112
@allowed_permissions = nil
-
112
@actions_allowed = nil
-
112
super
-
end
-
-
1
def to_param
-
# id is used for projects with a numeric identifier (compatibility)
-
4924
@to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
-
end
-
-
1
def active?
-
12909
self.status == STATUS_ACTIVE
-
end
-
-
1
def archived?
-
9
self.status == STATUS_ARCHIVED
-
end
-
-
# Archives the project and its descendants
-
1
def archive
-
# Check that there is no issue of a non descendant project that is assigned
-
# to one of the project or descendant versions
-
v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
-
if v_ids.any? && Issue.find(:first, :include => :project,
-
:conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
-
" AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
-
return false
-
end
-
Project.transaction do
-
archive!
-
end
-
true
-
end
-
-
# Unarchives the project
-
# All its ancestors must be active
-
1
def unarchive
-
return false if ancestors.detect {|a| !a.active?}
-
update_attribute :status, STATUS_ACTIVE
-
end
-
-
# Returns an array of projects the project can be moved to
-
# by the current user
-
1
def allowed_parents
-
return @allowed_parents if @allowed_parents
-
@allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
-
@allowed_parents = @allowed_parents - self_and_descendants
-
if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
-
@allowed_parents << nil
-
end
-
unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
-
@allowed_parents << parent
-
end
-
@allowed_parents
-
end
-
-
# Sets the parent of the project with authorization check
-
1
def set_allowed_parent!(p)
-
unless p.nil? || p.is_a?(Project)
-
if p.to_s.blank?
-
p = nil
-
else
-
p = Project.find_by_id(p)
-
return false unless p
-
end
-
end
-
if p.nil?
-
if !new_record? && allowed_parents.empty?
-
return false
-
end
-
elsif !allowed_parents.include?(p)
-
return false
-
end
-
set_parent!(p)
-
end
-
-
# Sets the parent of the project
-
# Argument can be either a Project, a String, a Fixnum or nil
-
1
def set_parent!(p)
-
28
unless p.nil? || p.is_a?(Project)
-
if p.to_s.blank?
-
p = nil
-
else
-
p = Project.find_by_id(p)
-
return false unless p
-
end
-
end
-
28
if p == parent && !p.nil?
-
# Nothing to do
-
true
-
28
elsif p.nil? || (p.active? && move_possible?(p))
-
# Insert the project so that target's children or root projects stay alphabetically sorted
-
28
sibs = (p.nil? ? self.class.roots : p.children)
-
38
to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
-
28
if to_be_inserted_before
-
move_to_left_of(to_be_inserted_before)
-
elsif p.nil?
-
if sibs.empty?
-
# move_to_root adds the project in first (ie. left) position
-
move_to_root
-
else
-
move_to_right_of(sibs.last) unless self == sibs.last
-
end
-
else
-
# move_to_child_of adds the project in last (ie.right) position
-
28
move_to_child_of(p)
-
end
-
28
Issue.update_versions_from_hierarchy_change(self)
-
28
true
-
else
-
# Can not move to the given target
-
false
-
end
-
end
-
-
# Returns an array of the trackers used by the project and its active sub projects
-
1
def rolled_up_trackers
-
@rolled_up_trackers ||=
-
Tracker.find(:all, :joins => :projects,
-
:select => "DISTINCT #{Tracker.table_name}.*",
-
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
-
90
:order => "#{Tracker.table_name}.position")
-
end
-
-
# Closes open and locked project versions that are completed
-
1
def close_completed_versions
-
Version.transaction do
-
versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
-
if version.completed?
-
version.update_attribute(:status, 'closed')
-
end
-
end
-
end
-
end
-
-
# Returns a scope of the Versions on subprojects
-
1
def rolled_up_versions
-
@rolled_up_versions ||=
-
Version.scoped(:include => :project,
-
:conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
-
end
-
-
# Returns a scope of the Versions used by the project
-
1
def shared_versions
-
1278
if new_record?
-
Version.scoped(:include => :project,
-
:conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'")
-
else
-
@shared_versions ||= begin
-
1227
r = root? ? self : root
-
1227
Version.scoped(:include => :project,
-
:conditions => "#{Project.table_name}.id = #{id}" +
-
" OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
-
" #{Version.table_name}.sharing = 'system'" +
-
" OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
-
" OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
-
" OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
-
"))")
-
1278
end
-
end
-
end
-
-
# Returns a hash of project users grouped by role
-
1
def users_by_role
-
67
members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
-
117
m.roles.each do |r|
-
117
h[r] ||= []
-
117
h[r] << m.user
-
end
-
117
h
-
end
-
end
-
-
# Deletes all project's members
-
1
def delete_all_members
-
me, mr = Member.table_name, MemberRole.table_name
-
connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
-
Member.delete_all(['project_id = ?', id])
-
end
-
-
# Users/groups issues can be assigned to
-
1
def assignable_users
-
92
assignable = Setting.issue_group_assignment? ? member_principals : members
-
644
assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
-
end
-
-
# Returns the mail adresses of users that should be always notified on project events
-
1
def recipients
-
notified_users.collect {|user| user.mail}
-
end
-
-
# Returns the users that should be notified on project events
-
1
def notified_users
-
# TODO: User part should be extracted to User#notify_about?
-
4937
members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
-
end
-
-
# Returns an array of all custom fields enabled for project issues
-
# (explictly associated custom fields and custom fields enabled for all projects)
-
1
def all_issue_custom_fields
-
1500
@all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
-
end
-
-
# Returns an array of all custom fields enabled for project time entries
-
# (explictly associated custom fields and custom fields enabled for all projects)
-
1
def all_time_entry_custom_fields
-
@all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
-
end
-
-
1
def project
-
self
-
end
-
-
1
def <=>(project)
-
name.downcase <=> project.name.downcase
-
end
-
-
1
def to_s
-
2441
name
-
end
-
-
# Returns a short description of the projects (first lines)
-
1
def short_description(length = 255)
-
16
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
-
end
-
-
1
def css_classes
-
s = 'project'
-
s << ' root' if root?
-
s << ' child' if child?
-
s << (leaf? ? ' leaf' : ' parent')
-
s
-
end
-
-
# The earliest start date of a project, based on it's issues and versions
-
1
def start_date
-
[
-
issues.minimum('start_date'),
-
shared_versions.collect(&:effective_date),
-
shared_versions.collect(&:start_date)
-
].flatten.compact.min
-
end
-
-
# The latest due date of an issue or version
-
1
def due_date
-
[
-
issues.maximum('due_date'),
-
shared_versions.collect(&:effective_date),
-
shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
-
].flatten.compact.max
-
end
-
-
1
def overdue?
-
active? && !due_date.nil? && (due_date < Date.today)
-
end
-
-
# Returns the percent completed for this project, based on the
-
# progress on it's versions.
-
1
def completed_percent(options={:include_subprojects => false})
-
if options.delete(:include_subprojects)
-
total = self_and_descendants.collect(&:completed_percent).sum
-
-
total / self_and_descendants.count
-
else
-
if versions.count > 0
-
total = versions.collect(&:completed_pourcent).sum
-
-
total / versions.count
-
else
-
100
-
end
-
end
-
end
-
-
# Return true if this project is allowed to do the specified action.
-
# action can be:
-
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
-
# * a permission Symbol (eg. :edit_project)
-
1
def allows_to?(action)
-
6045
if action.is_a? Hash
-
2245
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
-
else
-
3800
allowed_permissions.include? action
-
end
-
end
-
-
1
def module_enabled?(module_name)
-
238
module_name = module_name.to_s
-
2234
enabled_modules.detect {|m| m.name == module_name}
-
end
-
-
1
def enabled_module_names=(module_names)
-
46
if module_names && module_names.is_a?(Array)
-
46
module_names = module_names.collect(&:to_s).reject(&:blank?)
-
506
self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
-
else
-
enabled_modules.clear
-
end
-
end
-
-
# Returns an array of the enabled modules names
-
1
def enabled_module_names
-
2992
enabled_modules.collect(&:name)
-
end
-
-
# Enable a specific module
-
#
-
# Examples:
-
# project.enable_module!(:issue_tracking)
-
# project.enable_module!("issue_tracking")
-
1
def enable_module!(name)
-
238
enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
-
end
-
-
# Disable a module if it exists
-
#
-
# Examples:
-
# project.disable_module!(:issue_tracking)
-
# project.disable_module!("issue_tracking")
-
# project.disable_module!(project.enabled_modules.first)
-
1
def disable_module!(target)
-
target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
-
target.destroy unless target.blank?
-
end
-
-
1
safe_attributes 'name',
-
'description',
-
'homepage',
-
'is_public',
-
'identifier',
-
'custom_field_values',
-
'custom_fields',
-
'tracker_ids',
-
'issue_custom_field_ids'
-
-
1
safe_attributes 'enabled_module_names',
-
:if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
-
-
# Returns an array of projects that are in this project's hierarchy
-
#
-
# Example: parents, children, siblings
-
1
def hierarchy
-
parents = project.self_and_ancestors || []
-
descendants = project.descendants || []
-
project_hierarchy = parents | descendants # Set union
-
end
-
-
# Returns an auto-generated project identifier based on the last identifier used
-
1
def self.next_identifier
-
p = Project.find(:first, :order => 'created_on DESC')
-
p.nil? ? nil : p.identifier.to_s.succ
-
end
-
-
# Copies and saves the Project instance based on the +project+.
-
# Duplicates the source project's:
-
# * Wiki
-
# * Versions
-
# * Categories
-
# * Issues
-
# * Members
-
# * Queries
-
#
-
# Accepts an +options+ argument to specify what to copy
-
#
-
# Examples:
-
# project.copy(1) # => copies everything
-
# project.copy(1, :only => 'members') # => copies members only
-
# project.copy(1, :only => ['members', 'versions']) # => copies members and versions
-
1
def copy(project, options={})
-
project = project.is_a?(Project) ? project : Project.find(project)
-
-
to_be_copied = %w(wiki versions issue_categories issues members queries boards)
-
to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
-
-
Project.transaction do
-
if save
-
reload
-
to_be_copied.each do |name|
-
send "copy_#{name}", project
-
end
-
Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
-
save
-
end
-
end
-
end
-
-
-
# Copies +project+ and returns the new instance. This will not save
-
# the copy
-
1
def self.copy_from(project)
-
begin
-
project = project.is_a?(Project) ? project : Project.find(project)
-
if project
-
# clear unique attributes
-
attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
-
copy = Project.new(attributes)
-
copy.enabled_modules = project.enabled_modules
-
copy.trackers = project.trackers
-
copy.custom_values = project.custom_values.collect {|v| v.clone}
-
copy.issue_custom_fields = project.issue_custom_fields
-
return copy
-
else
-
return nil
-
end
-
rescue ActiveRecord::RecordNotFound
-
return nil
-
end
-
end
-
-
# Yields the given block for each project with its level in the tree
-
1
def self.project_tree(projects, &block)
-
343
ancestors = []
-
343
projects.sort_by(&:lft).each do |project|
-
997
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
-
620
ancestors.pop
-
end
-
997
yield project, ancestors.size
-
997
ancestors << project
-
end
-
end
-
-
1
private
-
-
# Copies wiki from +project+
-
1
def copy_wiki(project)
-
# Check that the source project has a wiki first
-
unless project.wiki.nil?
-
self.wiki ||= Wiki.new
-
wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
-
wiki_pages_map = {}
-
project.wiki.pages.each do |page|
-
# Skip pages without content
-
next if page.content.nil?
-
new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
-
new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
-
new_wiki_page.content = new_wiki_content
-
wiki.pages << new_wiki_page
-
wiki_pages_map[page.id] = new_wiki_page
-
end
-
wiki.save
-
# Reproduce page hierarchy
-
project.wiki.pages.each do |page|
-
if page.parent_id && wiki_pages_map[page.id]
-
wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
-
wiki_pages_map[page.id].save
-
end
-
end
-
end
-
end
-
-
# Copies versions from +project+
-
1
def copy_versions(project)
-
project.versions.each do |version|
-
new_version = Version.new
-
new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
-
self.versions << new_version
-
end
-
end
-
-
# Copies issue categories from +project+
-
1
def copy_issue_categories(project)
-
project.issue_categories.each do |issue_category|
-
new_issue_category = IssueCategory.new
-
new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
-
self.issue_categories << new_issue_category
-
end
-
end
-
-
# Copies issues from +project+
-
# Note: issues assigned to a closed version won't be copied due to validation rules
-
1
def copy_issues(project)
-
# Stores the source issue id as a key and the copied issues as the
-
# value. Used to map the two togeather for issue relations.
-
issues_map = {}
-
-
# Get issues sorted by root_id, lft so that parent issues
-
# get copied before their children
-
project.issues.find(:all, :order => 'root_id, lft').each do |issue|
-
new_issue = Issue.new
-
new_issue.copy_from(issue)
-
new_issue.project = self
-
# Reassign fixed_versions by name, since names are unique per
-
# project and the versions for self are not yet saved
-
if issue.fixed_version
-
new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
-
end
-
# Reassign the category by name, since names are unique per
-
# project and the categories for self are not yet saved
-
if issue.category
-
new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
-
end
-
# Parent issue
-
if issue.parent_id
-
if copied_parent = issues_map[issue.parent_id]
-
new_issue.parent_issue_id = copied_parent.id
-
end
-
end
-
-
self.issues << new_issue
-
if new_issue.new_record?
-
logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
-
else
-
issues_map[issue.id] = new_issue unless new_issue.new_record?
-
end
-
end
-
-
# Relations after in case issues related each other
-
project.issues.each do |issue|
-
new_issue = issues_map[issue.id]
-
unless new_issue
-
# Issue was not copied
-
next
-
end
-
-
# Relations
-
issue.relations_from.each do |source_relation|
-
new_issue_relation = IssueRelation.new
-
new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
-
new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
-
if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
-
new_issue_relation.issue_to = source_relation.issue_to
-
end
-
new_issue.relations_from << new_issue_relation
-
end
-
-
issue.relations_to.each do |source_relation|
-
new_issue_relation = IssueRelation.new
-
new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
-
new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
-
if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
-
new_issue_relation.issue_from = source_relation.issue_from
-
end
-
new_issue.relations_to << new_issue_relation
-
end
-
end
-
end
-
-
# Copies members from +project+
-
1
def copy_members(project)
-
# Copy users first, then groups to handle members with inherited and given roles
-
members_to_copy = []
-
members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
-
members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
-
-
members_to_copy.each do |member|
-
new_member = Member.new
-
new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
-
# only copy non inherited roles
-
# inherited roles will be added when copying the group membership
-
role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
-
next if role_ids.empty?
-
new_member.role_ids = role_ids
-
new_member.project = self
-
self.members << new_member
-
end
-
end
-
-
# Copies queries from +project+
-
1
def copy_queries(project)
-
project.queries.each do |query|
-
new_query = ::Query.new
-
new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
-
new_query.sort_criteria = query.sort_criteria if query.sort_criteria
-
new_query.project = self
-
new_query.user_id = query.user_id
-
self.queries << new_query
-
end
-
end
-
-
# Copies boards from +project+
-
1
def copy_boards(project)
-
project.boards.each do |board|
-
new_board = Board.new
-
new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
-
new_board.project = self
-
self.boards << new_board
-
end
-
end
-
-
1
def allowed_permissions
-
@allowed_permissions ||= begin
-
19983
module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
-
132500
Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
-
4180
end
-
end
-
-
1
def allowed_actions
-
31074
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
-
end
-
-
# Returns all the active Systemwide and project specific activities
-
1
def active_activities
-
4
overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
-
-
4
if overridden_activity_ids.empty?
-
4
return TimeEntryActivity.shared.active
-
else
-
return system_activities_and_project_overrides
-
end
-
end
-
-
# Returns all the Systemwide and project specific activities
-
# (inactive and active)
-
1
def all_activities
-
overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
-
-
if overridden_activity_ids.empty?
-
return TimeEntryActivity.shared
-
else
-
return system_activities_and_project_overrides(true)
-
end
-
end
-
-
# Returns the systemwide active activities merged with the project specific overrides
-
1
def system_activities_and_project_overrides(include_inactive=false)
-
if include_inactive
-
return TimeEntryActivity.shared.
-
find(:all,
-
:conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
-
self.time_entry_activities
-
else
-
return TimeEntryActivity.shared.active.
-
find(:all,
-
:conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
-
self.time_entry_activities.active
-
end
-
end
-
-
# Archives subprojects recursively
-
1
def archive!
-
children.each do |subproject|
-
subproject.send :archive!
-
end
-
update_attribute :status, STATUS_ARCHIVED
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ProjectCustomField < CustomField
-
1
def type_name
-
:label_project_plural
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class QueryColumn
-
1
attr_accessor :name, :sortable, :groupable, :default_order
-
1
include Redmine::I18n
-
-
1
def initialize(name, options={})
-
42
self.name = name
-
42
self.sortable = options[:sortable]
-
42
self.groupable = options[:groupable] || false
-
42
if groupable == true
-
9
self.groupable = name.to_s
-
end
-
42
self.default_order = options[:default_order]
-
42
@caption_key = options[:caption] || "field_#{name}"
-
end
-
-
1
def caption
-
498
l(@caption_key)
-
end
-
-
# Returns true if the column is sortable, otherwise false
-
1
def sortable?
-
!@sortable.nil?
-
end
-
-
1
def sortable
-
442
@sortable.is_a?(Proc) ? @sortable.call : @sortable
-
end
-
-
1
def value(issue)
-
508
issue.send name
-
end
-
-
1
def css_classes
-
508
name
-
end
-
end
-
-
1
class QueryCustomFieldColumn < QueryColumn
-
-
1
def initialize(custom_field)
-
110
self.name = "cf_#{custom_field.id}".to_sym
-
110
self.sortable = custom_field.order_statement || false
-
110
if %w(list date bool int).include?(custom_field.field_format) && !custom_field.multiple?
-
66
self.groupable = custom_field.order_statement
-
end
-
110
self.groupable ||= false
-
110
@cf = custom_field
-
end
-
-
1
def caption
-
112
@cf.name
-
end
-
-
1
def custom_field
-
@cf
-
end
-
-
1
def value(issue)
-
cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
-
cv.size > 1 ? cv : cv.first
-
end
-
-
1
def css_classes
-
@css_classes ||= "#{name} #{@cf.field_format}"
-
end
-
end
-
-
1
class Query < ActiveRecord::Base
-
1
class StatementInvalid < ::ActiveRecord::StatementInvalid
-
end
-
-
1
belongs_to :project
-
1
belongs_to :user
-
1
serialize :filters
-
1
serialize :column_names
-
1
serialize :sort_criteria, Array
-
-
1
attr_protected :project_id, :user_id
-
-
1
validates_presence_of :name
-
1
validates_length_of :name, :maximum => 255
-
1
validate :validate_query_filters
-
-
1
@@operators = { "=" => :label_equals,
-
"!" => :label_not_equals,
-
"o" => :label_open_issues,
-
"c" => :label_closed_issues,
-
"!*" => :label_none,
-
"*" => :label_all,
-
">=" => :label_greater_or_equal,
-
"<=" => :label_less_or_equal,
-
"><" => :label_between,
-
"<t+" => :label_in_less_than,
-
">t+" => :label_in_more_than,
-
"t+" => :label_in,
-
"t" => :label_today,
-
"w" => :label_this_week,
-
">t-" => :label_less_than_ago,
-
"<t-" => :label_more_than_ago,
-
"t-" => :label_ago,
-
"~" => :label_contains,
-
"!~" => :label_not_contains }
-
-
1
cattr_reader :operators
-
-
1
@@operators_by_filter_type = { :list => [ "=", "!" ],
-
:list_status => [ "o", "=", "!", "c", "*" ],
-
:list_optional => [ "=", "!", "!*", "*" ],
-
:list_subprojects => [ "*", "!*", "=" ],
-
:date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
-
:date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
-
:string => [ "=", "~", "!", "!~", "!*", "*" ],
-
:text => [ "~", "!~", "!*", "*" ],
-
:integer => [ "=", ">=", "<=", "><", "!*", "*" ],
-
:float => [ "=", ">=", "<=", "><", "!*", "*" ] }
-
-
1
cattr_reader :operators_by_filter_type
-
-
1
@@available_columns = [
-
QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
-
QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
-
QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
-
QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
-
QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
-
QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
-
14
QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
-
26
QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
-
QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
-
QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
-
QueryColumn.new(:fixed_version, :sortable => ["#{Version.table_name}.effective_date", "#{Version.table_name}.name"], :default_order => 'desc', :groupable => true),
-
QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
-
QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
-
QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
-
QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
-
QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
-
]
-
1
cattr_reader :available_columns
-
-
1
scope :visible, lambda {|*args|
-
16
user = args.shift || User.current
-
16
base = Project.allowed_to_condition(user, :view_issues, *args)
-
16
user_id = user.logged? ? user.id : 0
-
{
-
:conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
-
:include => :project
-
16
}
-
}
-
-
1
def initialize(attributes=nil, *args)
-
23
super attributes
-
23
self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
-
23
@is_for_all = project.nil?
-
end
-
-
1
def validate_query_filters
-
56
filters.each_key do |field|
-
128
if values_for(field)
-
128
case type_for(field)
-
when :integer
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
-
when :float
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+(\.\d*)?$/) }
-
when :date, :date_past
-
case operator_for(field)
-
when "=", ">=", "<=", "><"
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && (!v.match(/^\d{4}-\d{2}-\d{2}$/) || (Date.parse(v) rescue nil).nil?) }
-
when ">t-", "<t-", "t-"
-
add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
-
end
-
end
-
end
-
-
add_filter_error(field, :blank) unless
-
# filter requires one or more values
-
(values_for(field) and !values_for(field).first.blank?) or
-
# filter doesn't require any value
-
128
["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
-
56
end if filters
-
end
-
-
1
def add_filter_error(field, message)
-
m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
-
errors.add(:base, m)
-
end
-
-
# Returns true if the query is visible to +user+ or the current user.
-
1
def visible?(user=User.current)
-
(project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
-
end
-
-
1
def editable_by?(user)
-
return false unless user
-
# Admin can edit them all and regular users can edit their private queries
-
return true if user.admin? || (!is_public && self.user_id == user.id)
-
# Members can not edit public queries that are for all project (only admin is allowed to)
-
is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
-
end
-
-
1
def available_filters
-
367
return @available_filters if @available_filters
-
-
23
trackers = project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
-
-
161
@available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
-
115
"tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
-
138
"priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
-
"subject" => { :type => :text, :order => 8 },
-
"created_on" => { :type => :date_past, :order => 9 },
-
"updated_on" => { :type => :date_past, :order => 10 },
-
"start_date" => { :type => :date, :order => 11 },
-
"due_date" => { :type => :date, :order => 12 },
-
"estimated_hours" => { :type => :float, :order => 13 },
-
"done_ratio" => { :type => :integer, :order => 14 }}
-
-
23
principals = []
-
23
if project
-
23
principals += project.principals.sort
-
23
unless project.leaf?
-
23
subprojects = project.descendants.visible.all
-
23
if subprojects.any?
-
115
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
-
23
principals += Principal.member_of(subprojects)
-
end
-
end
-
else
-
all_projects = Project.visible.all
-
if all_projects.any?
-
# members of visible projects
-
principals += Principal.member_of(all_projects)
-
-
# project filter
-
project_values = []
-
if User.current.logged? && User.current.memberships.any?
-
project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
-
end
-
Project.project_tree(all_projects) do |p, level|
-
prefix = (level > 0 ? ('--' * level + ' ') : '')
-
project_values << ["#{prefix}#{p.name}", p.id.to_s]
-
end
-
@available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
-
end
-
end
-
23
principals.uniq!
-
23
principals.sort!
-
138
users = principals.select {|p| p.is_a?(User)}
-
-
23
assigned_to_values = []
-
23
assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
-
115
assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
-
23
@available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
-
-
23
author_values = []
-
23
author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
-
115
author_values += users.collect{|s| [s.name, s.id.to_s] }
-
23
@available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
-
-
69
group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
-
23
@available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
-
-
92
role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
-
23
@available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
-
-
23
if User.current.logged?
-
23
@available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
-
end
-
-
23
if project
-
# project specific filters
-
23
categories = project.issue_categories.all
-
23
unless categories.empty?
-
69
@available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
-
end
-
23
versions = project.shared_versions.all
-
23
unless versions.empty?
-
130
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
-
end
-
23
add_custom_fields_filters(project.all_issue_custom_fields)
-
else
-
# global filters for cross project issue list
-
system_shared_versions = Version.visible.find_all_by_sharing('system')
-
unless system_shared_versions.empty?
-
@available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
-
end
-
add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
-
end
-
23
@available_filters
-
end
-
-
1
def add_filter(field, operator, values)
-
# values must be an array
-
27
return unless values.nil? || values.is_a?(Array)
-
# check if field is defined as an available filter
-
27
if available_filters.has_key? field
-
27
filter_options = available_filters[field]
-
# check if operator is allowed for that filter
-
#if @@operators_by_filter_type[filter_options[:type]].include? operator
-
# allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
-
# filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
-
#end
-
27
filters[field] = {:operator => operator, :values => (values || [''])}
-
end
-
end
-
-
1
def add_short_filter(field, expression)
-
return unless expression && available_filters.has_key?(field)
-
field_type = available_filters[field][:type]
-
@@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
-
next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
-
add_filter field, operator, $1.present? ? $1.split('|') : ['']
-
end || add_filter(field, '=', expression.split('|'))
-
end
-
-
# Add multiple filters using +add_filter+
-
1
def add_filters(fields, operators, values)
-
if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
-
fields.each do |field|
-
add_filter(field, operators[field], values && values[field])
-
end
-
end
-
end
-
-
1
def has_filter?(field)
-
2548
filters and filters[field]
-
end
-
-
1
def type_for(field)
-
140
available_filters[field][:type] if available_filters.has_key?(field)
-
end
-
-
1
def operator_for(field)
-
440
has_filter?(field) ? filters[field][:operator] : nil
-
end
-
-
1
def values_for(field)
-
1156
has_filter?(field) ? filters[field][:values] : nil
-
end
-
-
1
def value_for(field, index=0)
-
294
(values_for(field) || [])[index]
-
end
-
-
1
def label_for(field)
-
label = available_filters[field][:name] if available_filters.has_key?(field)
-
label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
-
end
-
-
1
def available_columns
-
970
return @available_columns if @available_columns
-
22
@available_columns = ::Query.available_columns.dup
-
22
@available_columns += (project ?
-
project.all_issue_custom_fields :
-
IssueCustomField.find(:all)
-
110
).collect {|cf| QueryCustomFieldColumn.new(cf) }
-
-
22
if User.current.allowed_to?(:view_time_entries, project, :global => true)
-
22
index = nil
-
572
@available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
-
22
index = (index ? index + 1 : -1)
-
# insert the column after estimated_hours or at the end
-
22
@available_columns.insert index, QueryColumn.new(:spent_hours,
-
:sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
-
:default_order => 'desc',
-
:caption => :label_spent_time
-
)
-
end
-
22
@available_columns
-
end
-
-
1
def self.available_columns=(v)
-
self.available_columns = (v)
-
end
-
-
1
def self.add_available_column(column)
-
4
self.available_columns << (column) if column.is_a?(QueryColumn)
-
end
-
-
# Returns an array of columns that can be used to group the results
-
1
def groupable_columns
-
3348
available_columns.select {|c| c.groupable}
-
end
-
-
# Returns a Hash of columns and the key for sorting
-
1
def sortable_columns
-
14
{'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
-
364
h[column.name.to_s] = column.sortable
-
364
h
-
})
-
end
-
-
1
def columns
-
# preserve the column_names order
-
130
(has_default_columns? ? default_columns_names : column_names).collect do |name|
-
5998
available_columns.find { |col| col.name == name }
-
end.compact
-
end
-
-
1
def default_columns_names
-
@default_columns_names ||= begin
-
22
default_columns = Setting.issue_list_default_columns.map(&:to_sym)
-
-
22
project.present? ? default_columns : [:project] | default_columns
-
100
end
-
end
-
-
1
def column_names=(names)
-
14
if names
-
64
names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
-
64
names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
-
# Set column_names to nil if default columns
-
8
if names == default_columns_names
-
names = nil
-
end
-
end
-
14
write_attribute(:column_names, names)
-
end
-
-
1
def has_column?(column)
-
14
column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
-
end
-
-
1
def has_default_columns?
-
130
column_names.nil? || column_names.empty?
-
end
-
-
1
def sort_criteria=(arg)
-
c = []
-
if arg.is_a?(Hash)
-
arg = arg.keys.sort.collect {|k| arg[k]}
-
end
-
c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
-
write_attribute(:sort_criteria, c)
-
end
-
-
1
def sort_criteria
-
14
read_attribute(:sort_criteria) || []
-
end
-
-
1
def sort_criteria_key(arg)
-
sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
-
end
-
-
1
def sort_criteria_order(arg)
-
sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
-
end
-
-
# Returns the SQL sort order that should be prepended for grouping
-
1
def group_by_sort_order
-
14
if grouped? && (column = group_by_column)
-
column.sortable.is_a?(Array) ?
-
column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
-
"#{column.sortable} #{column.default_order}"
-
end
-
end
-
-
# Returns true if the query is a grouped query
-
1
def grouped?
-
110
!group_by_column.nil?
-
end
-
-
1
def group_by_column
-
1430
groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
-
end
-
-
1
def group_by_statement
-
group_by_column.try(:groupable)
-
end
-
-
1
def project_statement
-
28
project_clauses = []
-
28
if project && !project.descendants.active.empty?
-
28
ids = [project.id]
-
28
if has_filter?("subproject_id")
-
case operator_for("subproject_id")
-
when '='
-
# include the selected subprojects
-
ids += values_for("subproject_id").each(&:to_i)
-
when '!*'
-
# main project only
-
else
-
# all subprojects
-
ids += project.descendants.collect(&:id)
-
end
-
elsif Setting.display_subprojects_issues?
-
28
ids += project.descendants.collect(&:id)
-
end
-
28
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
-
elsif project
-
project_clauses << "#{Project.table_name}.id = %d" % project.id
-
end
-
28
project_clauses.any? ? project_clauses.join(' AND ') : nil
-
end
-
-
1
def statement
-
# filters clauses
-
28
filters_clauses = []
-
filters.each_key do |field|
-
64
next if field == "subproject_id"
-
64
v = values_for(field).clone
-
64
next unless v and !v.empty?
-
64
operator = operator_for(field)
-
-
# "me" value subsitution
-
64
if %w(assigned_to_id author_id watcher_id).include?(field)
-
if v.delete("me")
-
if User.current.logged?
-
v.push(User.current.id.to_s)
-
v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
-
else
-
v.push("0")
-
end
-
end
-
end
-
-
64
if field == 'project_id'
-
if v.delete('mine')
-
v += User.current.memberships.map(&:project_id).map(&:to_s)
-
end
-
end
-
-
64
if field =~ /^cf_(\d+)$/
-
# custom field
-
filters_clauses << sql_for_custom_field(field, operator, v, $1)
-
elsif respond_to?("sql_for_#{field}_field")
-
# specific statement
-
filters_clauses << send("sql_for_#{field}_field", field, operator, v)
-
else
-
# regular field
-
64
filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
-
end
-
28
end if filters and valid?
-
-
28
filters_clauses << project_statement
-
28
filters_clauses.reject!(&:blank?)
-
-
28
filters_clauses.any? ? filters_clauses.join(' AND ') : nil
-
end
-
-
# Returns the issue count
-
1
def issue_count
-
14
Issue.visible.count(:include => [:status, :project], :conditions => statement)
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the issue count by group or nil if query is not grouped
-
1
def issue_count_by_group
-
14
r = nil
-
14
if grouped?
-
begin
-
# Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
-
r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
-
rescue ActiveRecord::RecordNotFound
-
r = {nil => issue_count}
-
end
-
c = group_by_column
-
if c.is_a?(QueryCustomFieldColumn)
-
r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
-
end
-
end
-
14
r
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the issues
-
# Valid options are :order, :offset, :limit, :include, :conditions
-
1
def issues(options={})
-
42
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
-
14
order_option = nil if order_option.blank?
-
-
14
joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
-
-
14
issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
-
:conditions => statement,
-
:order => order_option,
-
:joins => joins,
-
:limit => options[:limit],
-
:offset => options[:offset]
-
-
14
if has_column?(:spent_hours)
-
Issue.load_visible_spent_hours(issues)
-
end
-
14
issues
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the issues ids
-
1
def issue_ids(options={})
-
order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
-
order_option = nil if order_option.blank?
-
-
joins = (order_option && order_option.include?('authors')) ? "LEFT OUTER JOIN users authors ON authors.id = #{Issue.table_name}.author_id" : nil
-
-
Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
-
:conditions => statement,
-
:order => order_option,
-
:joins => joins,
-
:limit => options[:limit],
-
:offset => options[:offset]).find_ids
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the journals
-
# Valid options are :order, :offset, :limit
-
1
def journals(options={})
-
Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
-
:conditions => statement,
-
:order => options[:order],
-
:limit => options[:limit],
-
:offset => options[:offset]
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
# Returns the versions
-
# Valid options are :conditions
-
1
def versions(options={})
-
Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
-
rescue ::ActiveRecord::StatementInvalid => e
-
raise StatementInvalid.new(e.message)
-
end
-
-
1
def sql_for_watcher_id_field(field, operator, value)
-
db_table = Watcher.table_name
-
"#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
-
sql_for_field(field, '=', value, db_table, 'user_id') + ')'
-
end
-
-
1
def sql_for_member_of_group_field(field, operator, value)
-
if operator == '*' # Any group
-
groups = Group.all
-
operator = '=' # Override the operator since we want to find by assigned_to
-
elsif operator == "!*"
-
groups = Group.all
-
operator = '!' # Override the operator since we want to find by assigned_to
-
else
-
groups = Group.find_all_by_id(value)
-
end
-
groups ||= []
-
-
members_of_groups = groups.inject([]) {|user_ids, group|
-
if group && group.user_ids.present?
-
user_ids << group.user_ids
-
end
-
user_ids.flatten.uniq.compact
-
}.sort.collect(&:to_s)
-
-
'(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
-
end
-
-
1
def sql_for_assigned_to_role_field(field, operator, value)
-
case operator
-
when "*", "!*" # Member / Not member
-
sw = operator == "!*" ? 'NOT' : ''
-
nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
-
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
-
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
-
when "=", "!"
-
role_cond = value.any? ?
-
"#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
-
"1=0"
-
-
sw = operator == "!" ? 'NOT' : ''
-
nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
-
"(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
-
" WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
-
end
-
end
-
-
1
private
-
-
1
def sql_for_custom_field(field, operator, value, custom_field_id)
-
db_table = CustomValue.table_name
-
db_field = 'value'
-
filter = @available_filters[field]
-
if filter && filter[:format] == 'user'
-
if value.delete('me')
-
value.push User.current.id.to_s
-
end
-
end
-
not_in = nil
-
if operator == '!'
-
# Makes ! operator work for custom fields with multiple values
-
operator = '='
-
not_in = 'NOT'
-
end
-
"#{Issue.table_name}.id #{not_in} IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{custom_field_id} WHERE " +
-
sql_for_field(field, operator, value, db_table, db_field, true) + ')'
-
end
-
-
# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
-
1
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
-
46
sql = ''
-
46
case operator
-
when "="
-
12
if value.any?
-
12
case type_for(field)
-
when :date, :date_past
-
sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
-
when :integer
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
-
else
-
sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
-
end
-
when :float
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
-
else
-
sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
-
end
-
else
-
24
sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
-
end
-
else
-
# IN an empty set
-
sql = "1=0"
-
end
-
when "!"
-
if value.any?
-
sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
-
else
-
# NOT IN an empty set
-
sql = "1=1"
-
end
-
when "!*"
-
6
sql = "#{db_table}.#{db_field} IS NULL"
-
6
sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
-
when "*"
-
10
sql = "#{db_table}.#{db_field} IS NOT NULL"
-
10
sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
-
when ">="
-
if [:date, :date_past].include?(type_for(field))
-
sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
-
else
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
-
else
-
sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
-
end
-
end
-
when "<="
-
if [:date, :date_past].include?(type_for(field))
-
sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
-
else
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
-
else
-
sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
-
end
-
end
-
when "><"
-
if [:date, :date_past].include?(type_for(field))
-
sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
-
else
-
if is_custom_filter
-
sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
-
else
-
sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
-
end
-
end
-
when "o"
-
18
sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
-
when "c"
-
sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
-
when ">t-"
-
sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
-
when "<t-"
-
sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
-
when "t-"
-
sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
-
when ">t+"
-
sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
-
when "<t+"
-
sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
-
when "t+"
-
sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
-
when "t"
-
sql = relative_date_clause(db_table, db_field, 0, 0)
-
when "w"
-
first_day_of_week = l(:general_first_day_of_week).to_i
-
day_of_week = Date.today.cwday
-
days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
-
sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
-
when "~"
-
sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
-
when "!~"
-
sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
-
else
-
raise "Unknown query operator #{operator}"
-
end
-
-
46
return sql
-
end
-
-
1
def add_custom_fields_filters(custom_fields)
-
23
@available_filters ||= {}
-
-
23
custom_fields.select(&:is_filter?).each do |field|
-
69
case field.field_format
-
when "text"
-
options = { :type => :text, :order => 20 }
-
when "list"
-
23
options = { :type => :list_optional, :values => field.possible_values, :order => 20}
-
when "date"
-
23
options = { :type => :date, :order => 20 }
-
when "bool"
-
options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
-
when "int"
-
options = { :type => :integer, :order => 20 }
-
when "float"
-
options = { :type => :float, :order => 20 }
-
when "user", "version"
-
next unless project
-
values = field.possible_values_options(project)
-
if User.current.logged? && field.field_format == 'user'
-
values.unshift ["<< #{l(:label_me)} >>", "me"]
-
end
-
options = { :type => :list_optional, :values => values, :order => 20}
-
else
-
23
options = { :type => :string, :order => 20 }
-
end
-
69
@available_filters["cf_#{field.id}"] = options.merge({ :name => field.name, :format => field.field_format })
-
end
-
end
-
-
# Returns a SQL clause for a date or datetime field.
-
1
def date_clause(table, field, from, to)
-
s = []
-
if from
-
from_yesterday = from - 1
-
from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
-
if self.class.default_timezone == :utc
-
from_yesterday_time = from_yesterday_time.utc
-
end
-
s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
-
end
-
if to
-
to_time = Time.local(to.year, to.month, to.day)
-
if self.class.default_timezone == :utc
-
to_time = to_time.utc
-
end
-
s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
-
end
-
s.join(' AND ')
-
end
-
-
# Returns a SQL clause for a date or datetime field using relative dates.
-
1
def relative_date_clause(table, field, days_from, days_to)
-
date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class ScmFetchError < Exception; end
-
-
1
class Repository < ActiveRecord::Base
-
1
include Redmine::Ciphering
-
-
1
belongs_to :project
-
1
has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
-
1
has_many :filechanges, :class_name => 'Change', :through => :changesets
-
-
1
serialize :extra_info
-
-
1
before_save :check_default
-
-
# Raw SQL to delete changesets and changes in the database
-
# has_many :changesets, :dependent => :destroy is too slow for big repositories
-
1
before_destroy :clear_changesets
-
-
1
validates_length_of :password, :maximum => 255, :allow_nil => true
-
1
validates_length_of :identifier, :maximum => 255, :allow_blank => true
-
1
validates_presence_of :identifier, :unless => Proc.new { |r| r.is_default? || r.set_as_default? }
-
1
validates_uniqueness_of :identifier, :scope => :project_id, :allow_blank => true
-
1
validates_exclusion_of :identifier, :in => %w(show entry raw changes annotate diff show stats graph)
-
# donwcase letters, digits, dashes but not digits only
-
1
validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :allow_blank => true
-
# Checks if the SCM is enabled when creating a repository
-
1
validate :repo_create_validation, :on => :create
-
-
1
def repo_create_validation
-
unless Setting.enabled_scm.include?(self.class.name.demodulize)
-
errors.add(:type, :invalid)
-
end
-
end
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "log_encoding"
-
attr_name = "commit_logs_encoding"
-
end
-
super(attr_name, *args)
-
end
-
-
# Removes leading and trailing whitespace
-
1
def url=(arg)
-
write_attribute(:url, arg ? arg.to_s.strip : nil)
-
end
-
-
# Removes leading and trailing whitespace
-
1
def root_url=(arg)
-
write_attribute(:root_url, arg ? arg.to_s.strip : nil)
-
end
-
-
1
def password
-
read_ciphered_attribute(:password)
-
end
-
-
1
def password=(arg)
-
write_ciphered_attribute(:password, arg)
-
end
-
-
1
def scm_adapter
-
self.class.scm_adapter_class
-
end
-
-
1
def scm
-
unless @scm
-
@scm = self.scm_adapter.new(url, root_url,
-
login, password, path_encoding)
-
if root_url.blank? && @scm.root_url.present?
-
update_attribute(:root_url, @scm.root_url)
-
end
-
end
-
@scm
-
end
-
-
1
def scm_name
-
self.class.scm_name
-
end
-
-
1
def name
-
if identifier.present?
-
identifier
-
elsif is_default?
-
l(:field_repository_is_default)
-
else
-
scm_name
-
end
-
end
-
-
1
def identifier_param
-
if is_default?
-
nil
-
elsif identifier.present?
-
identifier
-
else
-
id.to_s
-
end
-
end
-
-
1
def <=>(repository)
-
if is_default?
-
-1
-
elsif repository.is_default?
-
1
-
else
-
identifier.to_s <=> repository.identifier.to_s
-
end
-
end
-
-
1
def self.find_by_identifier_param(param)
-
if param.to_s =~ /^\d+$/
-
find_by_id(param)
-
else
-
find_by_identifier(param)
-
end
-
end
-
-
1
def merge_extra_info(arg)
-
h = extra_info || {}
-
return h if arg.nil?
-
h.merge!(arg)
-
write_attribute(:extra_info, h)
-
end
-
-
1
def report_last_commit
-
true
-
end
-
-
1
def supports_cat?
-
scm.supports_cat?
-
end
-
-
1
def supports_annotate?
-
scm.supports_annotate?
-
end
-
-
1
def supports_all_revisions?
-
true
-
end
-
-
1
def supports_directory_revisions?
-
false
-
end
-
-
1
def supports_revision_graph?
-
false
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
scm.entry(path, identifier)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
scm.entries(path, identifier)
-
end
-
-
1
def branches
-
scm.branches
-
end
-
-
1
def tags
-
scm.tags
-
end
-
-
1
def default_branch
-
nil
-
end
-
-
1
def properties(path, identifier=nil)
-
scm.properties(path, identifier)
-
end
-
-
1
def cat(path, identifier=nil)
-
scm.cat(path, identifier)
-
end
-
-
1
def diff(path, rev, rev_to)
-
scm.diff(path, rev, rev_to)
-
end
-
-
1
def diff_format_revisions(cs, cs_to, sep=':')
-
text = ""
-
text << cs_to.format_identifier + sep if cs_to
-
text << cs.format_identifier if cs
-
text
-
end
-
-
# Returns a path relative to the url of the repository
-
1
def relative_path(path)
-
path
-
end
-
-
# Finds and returns a revision with a number or the beginning of a hash
-
1
def find_changeset_by_name(name)
-
return nil if name.blank?
-
s = name.to_s
-
changesets.find(:first, :conditions => (s.match(/^\d*$/) ?
-
["revision = ?", s] : ["revision LIKE ?", s + '%']))
-
end
-
-
1
def latest_changeset
-
@latest_changeset ||= changesets.find(:first)
-
end
-
-
# Returns the latest changesets for +path+
-
# Default behaviour is to search in cached changesets
-
1
def latest_changesets(path, rev, limit=10)
-
if path.blank?
-
changesets.find(
-
:all,
-
:include => :user,
-
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
-
:limit => limit)
-
else
-
filechanges.find(
-
:all,
-
:include => {:changeset => :user},
-
:conditions => ["path = ?", path.with_leading_slash],
-
:order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
-
:limit => limit
-
).collect(&:changeset)
-
end
-
end
-
-
1
def scan_changesets_for_issue_ids
-
self.changesets.each(&:scan_comment_for_issue_ids)
-
end
-
-
# Returns an array of committers usernames and associated user_id
-
1
def committers
-
@committers ||= Changeset.connection.select_rows(
-
"SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
-
end
-
-
# Maps committers username to a user ids
-
1
def committer_ids=(h)
-
if h.is_a?(Hash)
-
committers.each do |committer, user_id|
-
new_user_id = h[committer]
-
if new_user_id && (new_user_id.to_i != user_id.to_i)
-
new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
-
Changeset.update_all(
-
"user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }",
-
["repository_id = ? AND committer = ?", id, committer])
-
end
-
end
-
@committers = nil
-
@found_committer_users = nil
-
true
-
else
-
false
-
end
-
end
-
-
# Returns the Redmine User corresponding to the given +committer+
-
# It will return nil if the committer is not yet mapped and if no User
-
# with the same username or email was found
-
1
def find_committer_user(committer)
-
unless committer.blank?
-
@found_committer_users ||= {}
-
return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
-
-
user = nil
-
c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
-
if c && c.user
-
user = c.user
-
elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
-
username, email = $1.strip, $3
-
u = User.find_by_login(username)
-
u ||= User.find_by_mail(email) unless email.blank?
-
user = u
-
end
-
@found_committer_users[committer] = user
-
user
-
end
-
end
-
-
1
def repo_log_encoding
-
encoding = log_encoding.to_s.strip
-
encoding.blank? ? 'UTF-8' : encoding
-
end
-
-
# Fetches new changesets for all repositories of active projects
-
# Can be called periodically by an external script
-
# eg. ruby script/runner "Repository.fetch_changesets"
-
1
def self.fetch_changesets
-
Project.active.has_module(:repository).all.each do |project|
-
project.repositories.each do |repository|
-
begin
-
repository.fetch_changesets
-
rescue Redmine::Scm::Adapters::CommandFailed => e
-
logger.error "scm: error during fetching changesets: #{e.message}"
-
end
-
end
-
end
-
end
-
-
# scan changeset comments to find related and fixed issues for all repositories
-
1
def self.scan_changesets_for_issue_ids
-
find(:all).each(&:scan_changesets_for_issue_ids)
-
end
-
-
1
def self.scm_name
-
'Abstract'
-
end
-
-
1
def self.available_scm
-
subclasses.collect {|klass| [klass.scm_name, klass.name]}
-
end
-
-
1
def self.factory(klass_name, *args)
-
klass = "Repository::#{klass_name}".constantize
-
klass.new(*args)
-
rescue
-
nil
-
end
-
-
1
def self.scm_adapter_class
-
nil
-
end
-
-
1
def self.scm_command
-
ret = ""
-
begin
-
ret = self.scm_adapter_class.client_command if self.scm_adapter_class
-
rescue Exception => e
-
logger.error "scm: error during get command: #{e.message}"
-
end
-
ret
-
end
-
-
1
def self.scm_version_string
-
ret = ""
-
begin
-
ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
-
rescue Exception => e
-
logger.error "scm: error during get version string: #{e.message}"
-
end
-
ret
-
end
-
-
1
def self.scm_available
-
ret = false
-
begin
-
ret = self.scm_adapter_class.client_available if self.scm_adapter_class
-
rescue Exception => e
-
logger.error "scm: error during get scm available: #{e.message}"
-
end
-
ret
-
end
-
-
1
def set_as_default?
-
new_record? && project && !Repository.first(:conditions => {:project_id => project.id})
-
end
-
-
1
protected
-
-
1
def check_default
-
if !is_default? && set_as_default?
-
self.is_default = true
-
end
-
if is_default? && is_default_changed?
-
Repository.update_all(["is_default = ?", false], ["project_id = ?", project_id])
-
end
-
end
-
-
1
private
-
-
# Deletes repository data
-
1
def clear_changesets
-
cs = Changeset.table_name
-
ch = Change.table_name
-
ci = "#{table_name_prefix}changesets_issues#{table_name_suffix}"
-
cp = "#{table_name_prefix}changeset_parents#{table_name_suffix}"
-
-
connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
-
connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
-
connection.delete("DELETE FROM #{cp} WHERE #{cp}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
-
connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/bazaar_adapter'
-
-
1
class Repository::Bazaar < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url, :log_encoding
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::BazaarAdapter
-
end
-
-
1
def self.scm_name
-
'Bazaar'
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
entries = scm.entries(path, identifier)
-
if entries
-
entries.each do |e|
-
next if e.lastrev.revision.blank?
-
# Set the filesize unless browsing a specific revision
-
if identifier.nil? && e.is_file?
-
full_path = File.join(root_url, e.path)
-
e.size = File.stat(full_path).size if File.file?(full_path)
-
end
-
c = Change.find(
-
:first,
-
:include => :changeset,
-
:conditions => [
-
"#{Change.table_name}.revision = ? and #{Changeset.table_name}.repository_id = ?",
-
e.lastrev.revision,
-
id
-
],
-
:order => "#{Changeset.table_name}.revision DESC")
-
if c
-
e.lastrev.identifier = c.changeset.revision
-
e.lastrev.name = c.changeset.revision
-
e.lastrev.author = c.changeset.committer
-
end
-
end
-
end
-
end
-
-
1
def fetch_changesets
-
scm_info = scm.info
-
if scm_info
-
# latest revision found in database
-
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
-
# latest revision in the repository
-
scm_revision = scm_info.lastrev.identifier.to_i
-
if db_revision < scm_revision
-
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
-
identifier_from = db_revision + 1
-
while (identifier_from <= scm_revision)
-
# loads changesets by batches of 200
-
identifier_to = [identifier_from + 199, scm_revision].min
-
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
-
transaction do
-
revisions.reverse_each do |revision|
-
changeset = Changeset.create(:repository => self,
-
:revision => revision.identifier,
-
:committer => revision.author,
-
:committed_on => revision.time,
-
:scmid => revision.scmid,
-
:comments => revision.message)
-
-
revision.paths.each do |change|
-
Change.create(:changeset => changeset,
-
:action => change[:action],
-
:path => change[:path],
-
:revision => change[:revision])
-
end
-
end
-
end unless revisions.nil?
-
identifier_from = identifier_to + 1
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/cvs_adapter'
-
1
require 'digest/sha1'
-
-
1
class Repository::Cvs < Repository
-
1
validates_presence_of :url, :root_url, :log_encoding
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "root_url"
-
attr_name = "cvsroot"
-
elsif attr_name == "url"
-
attr_name = "cvs_module"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::CvsAdapter
-
end
-
-
1
def self.scm_name
-
'CVS'
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
rev = identifier.nil? ? nil : changesets.find_by_revision(identifier)
-
scm.entry(path, rev.nil? ? nil : rev.committed_on)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
rev = nil
-
if ! identifier.nil?
-
rev = changesets.find_by_revision(identifier)
-
return nil if rev.nil?
-
end
-
entries = scm.entries(path, rev.nil? ? nil : rev.committed_on)
-
if entries
-
entries.each() do |entry|
-
if ( ! entry.lastrev.nil? ) && ( ! entry.lastrev.revision.nil? )
-
change = filechanges.find_by_revision_and_path(
-
entry.lastrev.revision,
-
scm.with_leading_slash(entry.path) )
-
if change
-
entry.lastrev.identifier = change.changeset.revision
-
entry.lastrev.revision = change.changeset.revision
-
entry.lastrev.author = change.changeset.committer
-
# entry.lastrev.branch = change.branch
-
end
-
end
-
end
-
end
-
entries
-
end
-
-
1
def cat(path, identifier=nil)
-
rev = nil
-
if ! identifier.nil?
-
rev = changesets.find_by_revision(identifier)
-
return nil if rev.nil?
-
end
-
scm.cat(path, rev.nil? ? nil : rev.committed_on)
-
end
-
-
1
def annotate(path, identifier=nil)
-
rev = nil
-
if ! identifier.nil?
-
rev = changesets.find_by_revision(identifier)
-
return nil if rev.nil?
-
end
-
scm.annotate(path, rev.nil? ? nil : rev.committed_on)
-
end
-
-
1
def diff(path, rev, rev_to)
-
# convert rev to revision. CVS can't handle changesets here
-
diff=[]
-
changeset_from = changesets.find_by_revision(rev)
-
if rev_to.to_i > 0
-
changeset_to = changesets.find_by_revision(rev_to)
-
end
-
changeset_from.filechanges.each() do |change_from|
-
revision_from = nil
-
revision_to = nil
-
if path.nil? || (change_from.path.starts_with? scm.with_leading_slash(path))
-
revision_from = change_from.revision
-
end
-
if revision_from
-
if changeset_to
-
changeset_to.filechanges.each() do |change_to|
-
revision_to = change_to.revision if change_to.path == change_from.path
-
end
-
end
-
unless revision_to
-
revision_to = scm.get_previous_revision(revision_from)
-
end
-
file_diff = scm.diff(change_from.path, revision_from, revision_to)
-
diff = diff + file_diff unless file_diff.nil?
-
end
-
end
-
return diff
-
end
-
-
1
def fetch_changesets
-
# some nifty bits to introduce a commit-id with cvs
-
# natively cvs doesn't provide any kind of changesets,
-
# there is only a revision per file.
-
# we now take a guess using the author, the commitlog and the commit-date.
-
-
# last one is the next step to take. the commit-date is not equal for all
-
# commits in one changeset. cvs update the commit-date when the *,v file was touched. so
-
# we use a small delta here, to merge all changes belonging to _one_ changeset
-
time_delta = 10.seconds
-
fetch_since = latest_changeset ? latest_changeset.committed_on : nil
-
transaction do
-
tmp_rev_num = 1
-
scm.revisions('', fetch_since, nil, :log_encoding => repo_log_encoding) do |revision|
-
# only add the change to the database, if it doen't exists. the cvs log
-
# is not exclusive at all.
-
tmp_time = revision.time.clone
-
unless filechanges.find_by_path_and_revision(
-
scm.with_leading_slash(revision.paths[0][:path]),
-
revision.paths[0][:revision]
-
)
-
cmt = Changeset.normalize_comments(revision.message, repo_log_encoding)
-
author_utf8 = Changeset.to_utf8(revision.author, repo_log_encoding)
-
cs = changesets.find(
-
:first,
-
:conditions => {
-
:committed_on => tmp_time - time_delta .. tmp_time + time_delta,
-
:committer => author_utf8,
-
:comments => cmt
-
}
-
)
-
# create a new changeset....
-
unless cs
-
# we use a temporaray revision number here (just for inserting)
-
# later on, we calculate a continous positive number
-
tmp_time2 = tmp_time.clone.gmtime
-
branch = revision.paths[0][:branch]
-
scmid = branch + "-" + tmp_time2.strftime("%Y%m%d-%H%M%S")
-
cs = Changeset.create(:repository => self,
-
:revision => "tmp#{tmp_rev_num}",
-
:scmid => scmid,
-
:committer => revision.author,
-
:committed_on => tmp_time,
-
:comments => revision.message)
-
tmp_rev_num += 1
-
end
-
# convert CVS-File-States to internal Action-abbrevations
-
# default action is (M)odified
-
action = "M"
-
if revision.paths[0][:action] == "Exp" && revision.paths[0][:revision] == "1.1"
-
action = "A" # add-action always at first revision (= 1.1)
-
elsif revision.paths[0][:action] == "dead"
-
action = "D" # dead-state is similar to Delete
-
end
-
Change.create(
-
:changeset => cs,
-
:action => action,
-
:path => scm.with_leading_slash(revision.paths[0][:path]),
-
:revision => revision.paths[0][:revision],
-
:branch => revision.paths[0][:branch]
-
)
-
end
-
end
-
-
# Renumber new changesets in chronological order
-
Changeset.all(
-
:order => 'committed_on ASC, id ASC',
-
:conditions => ["repository_id = ? AND revision LIKE 'tmp%'", id]
-
).each do |changeset|
-
changeset.update_attribute :revision, next_revision_number
-
end
-
end # transaction
-
@current_revision_number = nil
-
end
-
-
1
private
-
-
# Returns the next revision number to assign to a CVS changeset
-
1
def next_revision_number
-
# Need to retrieve existing revision numbers to sort them as integers
-
sql = "SELECT revision FROM #{Changeset.table_name} "
-
sql << "WHERE repository_id = #{id} AND revision NOT LIKE 'tmp%'"
-
@current_revision_number ||= (connection.select_values(sql).collect(&:to_i).max || 0)
-
@current_revision_number += 1
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/darcs_adapter'
-
-
1
class Repository::Darcs < Repository
-
1
validates_presence_of :url, :log_encoding
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::DarcsAdapter
-
end
-
-
1
def self.scm_name
-
'Darcs'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier)
-
scm.entry(path, patch.nil? ? nil : patch.scmid)
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
patch = nil
-
if ! identifier.nil?
-
patch = changesets.find_by_revision(identifier)
-
return nil if patch.nil?
-
end
-
entries = scm.entries(path, patch.nil? ? nil : patch.scmid)
-
if entries
-
entries.each do |entry|
-
# Search the DB for the entry's last change
-
if entry.lastrev && !entry.lastrev.scmid.blank?
-
changeset = changesets.find_by_scmid(entry.lastrev.scmid)
-
end
-
if changeset
-
entry.lastrev.identifier = changeset.revision
-
entry.lastrev.name = changeset.revision
-
entry.lastrev.time = changeset.committed_on
-
entry.lastrev.author = changeset.committer
-
end
-
end
-
end
-
entries
-
end
-
-
1
def cat(path, identifier=nil)
-
patch = identifier.nil? ? nil : changesets.find_by_revision(identifier.to_s)
-
scm.cat(path, patch.nil? ? nil : patch.scmid)
-
end
-
-
1
def diff(path, rev, rev_to)
-
patch_from = changesets.find_by_revision(rev)
-
return nil if patch_from.nil?
-
patch_to = changesets.find_by_revision(rev_to) if rev_to
-
if path.blank?
-
path = patch_from.filechanges.collect{|change| change.path}.join(' ')
-
end
-
patch_from ? scm.diff(path, patch_from.scmid, patch_to ? patch_to.scmid : nil) : nil
-
end
-
-
1
def fetch_changesets
-
scm_info = scm.info
-
if scm_info
-
db_last_id = latest_changeset ? latest_changeset.scmid : nil
-
next_rev = latest_changeset ? latest_changeset.revision.to_i + 1 : 1
-
# latest revision in the repository
-
scm_revision = scm_info.lastrev.scmid
-
unless changesets.find_by_scmid(scm_revision)
-
revisions = scm.revisions('', db_last_id, nil, :with_path => true)
-
transaction do
-
revisions.reverse_each do |revision|
-
changeset = Changeset.create(:repository => self,
-
:revision => next_rev,
-
:scmid => revision.scmid,
-
:committer => revision.author,
-
:committed_on => revision.time,
-
:comments => revision.message)
-
revision.paths.each do |change|
-
changeset.create_change(change)
-
end
-
next_rev += 1
-
end if revisions
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# FileSystem adapter
-
# File written by Paul Rivier, at Demotera.
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/filesystem_adapter'
-
-
1
class Repository::Filesystem < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "root_directory"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::FilesystemAdapter
-
end
-
-
1
def self.scm_name
-
'Filesystem'
-
end
-
-
1
def supports_all_revisions?
-
false
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
scm.entries(path, identifier)
-
end
-
-
1
def fetch_changesets
-
nil
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
# Copyright (C) 2007 Patrick Aljord patcito@ŋmail.com
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/git_adapter'
-
-
1
class Repository::Git < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::GitAdapter
-
end
-
-
1
def self.scm_name
-
'Git'
-
end
-
-
1
def report_last_commit
-
extra_report_last_commit
-
end
-
-
1
def extra_report_last_commit
-
return false if extra_info.nil?
-
v = extra_info["extra_report_last_commit"]
-
return false if v.nil?
-
v.to_s != '0'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def supports_revision_graph?
-
true
-
end
-
-
1
def repo_log_encoding
-
'UTF-8'
-
end
-
-
# Returns the identifier for the given git changeset
-
1
def self.changeset_identifier(changeset)
-
changeset.scmid
-
end
-
-
# Returns the readable identifier for the given git changeset
-
1
def self.format_changeset_identifier(changeset)
-
changeset.revision[0, 8]
-
end
-
-
1
def branches
-
scm.branches
-
end
-
-
1
def tags
-
scm.tags
-
end
-
-
1
def default_branch
-
scm.default_branch
-
rescue Exception => e
-
logger.error "git: error during get default branch: #{e.message}"
-
nil
-
end
-
-
1
def find_changeset_by_name(name)
-
return nil if name.nil? || name.empty?
-
e = changesets.find(:first, :conditions => ['revision = ?', name.to_s])
-
return e if e
-
changesets.find(:first, :conditions => ['scmid LIKE ?', "#{name}%"])
-
end
-
-
1
def entries(path=nil, identifier=nil)
-
scm.entries(path,
-
identifier,
-
options = {:report_last_commit => extra_report_last_commit})
-
end
-
-
# With SCMs that have a sequential commit numbering,
-
# such as Subversion and Mercurial,
-
# Redmine is able to be clever and only fetch changesets
-
# going forward from the most recent one it knows about.
-
#
-
# However, Git does not have a sequential commit numbering.
-
#
-
# In order to fetch only new adding revisions,
-
# Redmine needs to save "heads".
-
#
-
# In Git and Mercurial, revisions are not in date order.
-
# Redmine Mercurial fixed issues.
-
# * Redmine Takes Too Long On Large Mercurial Repository
-
# http://www.redmine.org/issues/3449
-
# * Sorting for changesets might go wrong on Mercurial repos
-
# http://www.redmine.org/issues/3567
-
#
-
# Database revision column is text, so Redmine can not sort by revision.
-
# Mercurial has revision number, and revision number guarantees revision order.
-
# Redmine Mercurial model stored revisions ordered by database id to database.
-
# So, Redmine Mercurial model can use correct ordering revisions.
-
#
-
# Redmine Mercurial adapter uses "hg log -r 0:tip --limit 10"
-
# to get limited revisions from old to new.
-
# But, Git 1.7.3.4 does not support --reverse with -n or --skip.
-
#
-
# The repository can still be fully reloaded by calling #clear_changesets
-
# before fetching changesets (eg. for offline resync)
-
1
def fetch_changesets
-
scm_brs = branches
-
return if scm_brs.nil? || scm_brs.empty?
-
-
h1 = extra_info || {}
-
h = h1.dup
-
repo_heads = scm_brs.map{ |br| br.scmid }
-
h["heads"] ||= []
-
prev_db_heads = h["heads"].dup
-
if prev_db_heads.empty?
-
prev_db_heads += heads_from_branches_hash
-
end
-
return if prev_db_heads.sort == repo_heads.sort
-
-
h["db_consistent"] ||= {}
-
if changesets.count == 0
-
h["db_consistent"]["ordering"] = 1
-
merge_extra_info(h)
-
self.save
-
elsif ! h["db_consistent"].has_key?("ordering")
-
h["db_consistent"]["ordering"] = 0
-
merge_extra_info(h)
-
self.save
-
end
-
save_revisions(prev_db_heads, repo_heads)
-
end
-
-
1
def save_revisions(prev_db_heads, repo_heads)
-
h = {}
-
opts = {}
-
opts[:reverse] = true
-
opts[:excludes] = prev_db_heads
-
opts[:includes] = repo_heads
-
-
revisions = scm.revisions('', nil, nil, opts)
-
return if revisions.blank?
-
-
# Make the search for existing revisions in the database in a more sufficient manner
-
#
-
# Git branch is the reference to the specific revision.
-
# Git can *delete* remote branch and *re-push* branch.
-
#
-
# $ git push remote :branch
-
# $ git push remote branch
-
#
-
# After deleting branch, revisions remain in repository until "git gc".
-
# On git 1.7.2.3, default pruning date is 2 weeks.
-
# So, "git log --not deleted_branch_head_revision" return code is 0.
-
#
-
# After re-pushing branch, "git log" returns revisions which are saved in database.
-
# So, Redmine needs to scan revisions and database every time.
-
#
-
# This is replacing the one-after-one queries.
-
# Find all revisions, that are in the database, and then remove them from the revision array.
-
# Then later we won't need any conditions for db existence.
-
# Query for several revisions at once, and remove them from the revisions array, if they are there.
-
# Do this in chunks, to avoid eventual memory problems (in case of tens of thousands of commits).
-
# If there are no revisions (because the original code's algorithm filtered them),
-
# then this part will be stepped over.
-
# We make queries, just if there is any revision.
-
limit = 100
-
offset = 0
-
revisions_copy = revisions.clone # revisions will change
-
while offset < revisions_copy.size
-
recent_changesets_slice = changesets.find(
-
:all,
-
:conditions => [
-
'scmid IN (?)',
-
revisions_copy.slice(offset, limit).map{|x| x.scmid}
-
]
-
)
-
# Subtract revisions that redmine already knows about
-
recent_revisions = recent_changesets_slice.map{|c| c.scmid}
-
revisions.reject!{|r| recent_revisions.include?(r.scmid)}
-
offset += limit
-
end
-
-
revisions.each do |rev|
-
transaction do
-
# There is no search in the db for this revision, because above we ensured,
-
# that it's not in the db.
-
save_revision(rev)
-
end
-
end
-
h["heads"] = repo_heads.dup
-
merge_extra_info(h)
-
self.save
-
end
-
1
private :save_revisions
-
-
1
def save_revision(rev)
-
parents = (rev.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
-
changeset = Changeset.create(
-
:repository => self,
-
:revision => rev.identifier,
-
:scmid => rev.scmid,
-
:committer => rev.author,
-
:committed_on => rev.time,
-
:comments => rev.message,
-
:parents => parents
-
)
-
unless changeset.new_record?
-
rev.paths.each { |change| changeset.create_change(change) }
-
end
-
changeset
-
end
-
1
private :save_revision
-
-
1
def heads_from_branches_hash
-
h1 = extra_info || {}
-
h = h1.dup
-
h["branches"] ||= {}
-
h['branches'].map{|br, hs| hs['last_scmid']}
-
end
-
-
1
def latest_changesets(path,rev,limit=10)
-
revisions = scm.revisions(path, nil, rev, :limit => limit, :all => false)
-
return [] if revisions.nil? || revisions.empty?
-
-
changesets.find(
-
:all,
-
:conditions => [
-
"scmid IN (?)",
-
revisions.map!{|c| c.scmid}
-
],
-
:order => 'committed_on DESC'
-
)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/mercurial_adapter'
-
-
1
class Repository::Mercurial < Repository
-
# sort changesets by revision number
-
1
has_many :changesets,
-
:order => "#{Changeset.table_name}.id DESC",
-
:foreign_key => 'repository_id'
-
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
-
# number of changesets to fetch at once
-
1
FETCH_AT_ONCE = 100
-
-
1
def self.human_attribute_name(attribute_key_name, *args)
-
attr_name = attribute_key_name.to_s
-
if attr_name == "url"
-
attr_name = "path_to_repository"
-
end
-
super(attr_name, *args)
-
end
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::MercurialAdapter
-
end
-
-
1
def self.scm_name
-
'Mercurial'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def supports_revision_graph?
-
true
-
end
-
-
1
def repo_log_encoding
-
'UTF-8'
-
end
-
-
# Returns the readable identifier for the given mercurial changeset
-
1
def self.format_changeset_identifier(changeset)
-
"#{changeset.revision}:#{changeset.scmid}"
-
end
-
-
# Returns the identifier for the given Mercurial changeset
-
1
def self.changeset_identifier(changeset)
-
changeset.scmid
-
end
-
-
1
def diff_format_revisions(cs, cs_to, sep=':')
-
super(cs, cs_to, ' ')
-
end
-
-
# Finds and returns a revision with a number or the beginning of a hash
-
1
def find_changeset_by_name(name)
-
return nil if name.blank?
-
s = name.to_s
-
if /[^\d]/ =~ s or s.size > 8
-
e = changesets.find(:first, :conditions => ['scmid = ?', s])
-
else
-
e = changesets.find(:first, :conditions => ['revision = ?', s])
-
end
-
return e if e
-
changesets.find(:first, :conditions => ['scmid LIKE ?', "#{s}%"]) # last ditch
-
end
-
-
# Returns the latest changesets for +path+; sorted by revision number
-
#
-
# Because :order => 'id DESC' is defined at 'has_many',
-
# there is no need to set 'order'.
-
# But, MySQL test fails.
-
# Sqlite3 and PostgreSQL pass.
-
# Is this MySQL bug?
-
1
def latest_changesets(path, rev, limit=10)
-
changesets.find(:all,
-
:include => :user,
-
:conditions => latest_changesets_cond(path, rev, limit),
-
:limit => limit,
-
:order => "#{Changeset.table_name}.id DESC")
-
end
-
-
1
def latest_changesets_cond(path, rev, limit)
-
cond, args = [], []
-
if scm.branchmap.member? rev
-
# Mercurial named branch is *stable* in each revision.
-
# So, named branch can be stored in database.
-
# Mercurial provides *bookmark* which is equivalent with git branch.
-
# But, bookmark is not implemented.
-
cond << "#{Changeset.table_name}.scmid IN (?)"
-
# Revisions in root directory and sub directory are not equal.
-
# So, in order to get correct limit, we need to get all revisions.
-
# But, it is very heavy.
-
# Mercurial does not treat direcotry.
-
# So, "hg log DIR" is very heavy.
-
branch_limit = path.blank? ? limit : ( limit * 5 )
-
args << scm.nodes_in_branch(rev, :limit => branch_limit)
-
elsif last = rev ? find_changeset_by_name(scm.tagmap[rev] || rev) : nil
-
cond << "#{Changeset.table_name}.id <= ?"
-
args << last.id
-
end
-
unless path.blank?
-
cond << "EXISTS (SELECT * FROM #{Change.table_name}
-
WHERE #{Change.table_name}.changeset_id = #{Changeset.table_name}.id
-
AND (#{Change.table_name}.path = ?
-
OR #{Change.table_name}.path LIKE ? ESCAPE ?))"
-
args << path.with_leading_slash
-
args << "#{path.with_leading_slash.gsub(%r{[%_\\]}) { |s| "\\#{s}" }}/%" << '\\'
-
end
-
[cond.join(' AND '), *args] unless cond.empty?
-
end
-
1
private :latest_changesets_cond
-
-
1
def fetch_changesets
-
return if scm.info.nil?
-
scm_rev = scm.info.lastrev.revision.to_i
-
db_rev = latest_changeset ? latest_changeset.revision.to_i : -1
-
return unless db_rev < scm_rev # already up-to-date
-
-
logger.debug "Fetching changesets for repository #{url}" if logger
-
(db_rev + 1).step(scm_rev, FETCH_AT_ONCE) do |i|
-
scm.each_revision('', i, [i + FETCH_AT_ONCE - 1, scm_rev].min) do |re|
-
transaction do
-
parents = (re.parents || []).collect{|rp| find_changeset_by_name(rp)}.compact
-
cs = Changeset.create(:repository => self,
-
:revision => re.revision,
-
:scmid => re.scmid,
-
:committer => re.author,
-
:committed_on => re.time,
-
:comments => re.message,
-
:parents => parents)
-
unless cs.new_record?
-
re.paths.each { |e| cs.create_change(e) }
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/subversion_adapter'
-
-
1
class Repository::Subversion < Repository
-
1
attr_protected :root_url
-
1
validates_presence_of :url
-
1
validates_format_of :url, :with => /^(http|https|svn(\+[^\s:\/\\]+)?|file):\/\/.+/i
-
-
1
def self.scm_adapter_class
-
Redmine::Scm::Adapters::SubversionAdapter
-
end
-
-
1
def self.scm_name
-
'Subversion'
-
end
-
-
1
def supports_directory_revisions?
-
true
-
end
-
-
1
def repo_log_encoding
-
'UTF-8'
-
end
-
-
1
def latest_changesets(path, rev, limit=10)
-
revisions = scm.revisions(path, rev, nil, :limit => limit)
-
revisions ? changesets.find_all_by_revision(revisions.collect(&:identifier), :order => "committed_on DESC", :include => :user) : []
-
end
-
-
# Returns a path relative to the url of the repository
-
1
def relative_path(path)
-
path.gsub(Regexp.new("^\/?#{Regexp.escape(relative_url)}"), '')
-
end
-
-
1
def fetch_changesets
-
scm_info = scm.info
-
if scm_info
-
# latest revision found in database
-
db_revision = latest_changeset ? latest_changeset.revision.to_i : 0
-
# latest revision in the repository
-
scm_revision = scm_info.lastrev.identifier.to_i
-
if db_revision < scm_revision
-
logger.debug "Fetching changesets for repository #{url}" if logger && logger.debug?
-
identifier_from = db_revision + 1
-
while (identifier_from <= scm_revision)
-
# loads changesets by batches of 200
-
identifier_to = [identifier_from + 199, scm_revision].min
-
revisions = scm.revisions('', identifier_to, identifier_from, :with_paths => true)
-
revisions.reverse_each do |revision|
-
transaction do
-
changeset = Changeset.create(:repository => self,
-
:revision => revision.identifier,
-
:committer => revision.author,
-
:committed_on => revision.time,
-
:comments => revision.message)
-
-
revision.paths.each do |change|
-
changeset.create_change(change)
-
end unless changeset.new_record?
-
end
-
end unless revisions.nil?
-
identifier_from = identifier_to + 1
-
end
-
end
-
end
-
end
-
-
1
private
-
-
# Returns the relative url of the repository
-
# Eg: root_url = file:///var/svn/foo
-
# url = file:///var/svn/foo/bar
-
# => returns /bar
-
1
def relative_url
-
@relative_url ||= url.gsub(Regexp.new("^#{Regexp.escape(root_url || scm.root_url)}", Regexp::IGNORECASE), '')
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Role < ActiveRecord::Base
-
# Built-in roles
-
1
BUILTIN_NON_MEMBER = 1
-
1
BUILTIN_ANONYMOUS = 2
-
-
1
ISSUES_VISIBILITY_OPTIONS = [
-
['all', :label_issues_visibility_all],
-
['default', :label_issues_visibility_public],
-
['own', :label_issues_visibility_own]
-
]
-
-
1
scope :sorted, {:order => 'builtin, position'}
-
1
scope :givable, { :conditions => "builtin = 0", :order => 'position' }
-
1
scope :builtin, lambda { |*args|
-
compare = 'not' if args.first == true
-
{ :conditions => "#{compare} builtin = 0" }
-
}
-
-
1
before_destroy :check_deletable
-
1
has_many :workflows, :dependent => :delete_all do
-
1
def copy(source_role)
-
Workflow.copy(nil, source_role, nil, proxy_association.owner)
-
end
-
end
-
-
1
has_many :member_roles, :dependent => :destroy
-
1
has_many :members, :through => :member_roles
-
1
acts_as_list
-
-
1
serialize :permissions, Array
-
1
attr_protected :builtin
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 30
-
1
validates_inclusion_of :issues_visibility,
-
:in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
-
360
:if => lambda {|role| role.respond_to?(:issues_visibility)}
-
-
1
def permissions
-
4840
read_attribute(:permissions) || []
-
end
-
-
1
def permissions=(perms)
-
perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
-
write_attribute(:permissions, perms)
-
end
-
-
1
def add_permission!(*perms)
-
self.permissions = [] unless permissions.is_a?(Array)
-
-
permissions_will_change!
-
perms.each do |p|
-
p = p.to_sym
-
permissions << p unless permissions.include?(p)
-
end
-
save!
-
end
-
-
1
def remove_permission!(*perms)
-
return unless permissions.is_a?(Array)
-
permissions_will_change!
-
perms.each { |p| permissions.delete(p.to_sym) }
-
save!
-
end
-
-
# Returns true if the role has the given permission
-
1
def has_permission?(perm)
-
!permissions.nil? && permissions.include?(perm.to_sym)
-
end
-
-
1
def <=>(role)
-
50
role ? position <=> role.position : -1
-
end
-
-
1
def to_s
-
117
name
-
end
-
-
1
def name
-
1266
case builtin
-
when 1; l(:label_role_non_member, :default => read_attribute(:name))
-
when 2; l(:label_role_anonymous, :default => read_attribute(:name))
-
1266
else; read_attribute(:name)
-
end
-
end
-
-
# Return true if the role is a builtin role
-
1
def builtin?
-
246
self.builtin != 0
-
end
-
-
# Return true if the role is a project member role
-
1
def member?
-
246
!self.builtin?
-
end
-
-
# Return true if role is allowed to do the specified action
-
# action can be:
-
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
-
# * a permission Symbol (eg. :edit_project)
-
1
def allowed_to?(action)
-
9195
if action.is_a? Hash
-
2571
allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
-
else
-
6624
allowed_permissions.include? action
-
end
-
end
-
-
# Return all the permissions that can be given to the role
-
1
def setable_permissions
-
setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
-
setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
-
setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
-
setable_permissions
-
end
-
-
# Find all the roles that can be given to a project member
-
1
def self.find_all_givable
-
find(:all, :conditions => {:builtin => 0}, :order => 'position')
-
end
-
-
# Return the builtin 'non member' role. If the role doesn't exist,
-
# it will be created on the fly.
-
1
def self.non_member
-
1038
find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
-
end
-
-
# Return the builtin 'anonymous' role. If the role doesn't exist,
-
# it will be created on the fly.
-
1
def self.anonymous
-
141
find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
-
end
-
-
1
private
-
-
1
def allowed_permissions
-
23188
@allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
-
end
-
-
1
def allowed_actions
-
41343
@actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
-
end
-
-
1
def check_deletable
-
raise "Can't delete role" if members.any?
-
raise "Can't delete builtin role" if builtin?
-
end
-
-
1
def self.find_or_create_system_role(builtin, name)
-
1179
role = first(:conditions => {:builtin => builtin})
-
1179
if role.nil?
-
role = create(:name => name, :position => 0) do |r|
-
r.builtin = builtin
-
end
-
raise "Unable to create the #{name} role." if role.new_record?
-
end
-
1179
role
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Setting < ActiveRecord::Base
-
-
1
DATE_FORMATS = [
-
'%Y-%m-%d',
-
'%d/%m/%Y',
-
'%d.%m.%Y',
-
'%d-%m-%Y',
-
'%m/%d/%Y',
-
'%d %b %Y',
-
'%d %B %Y',
-
'%b %d, %Y',
-
'%B %d, %Y'
-
]
-
-
1
TIME_FORMATS = [
-
'%H:%M',
-
'%I:%M %p'
-
]
-
-
1
ENCODINGS = %w(US-ASCII
-
windows-1250
-
windows-1251
-
windows-1252
-
windows-1253
-
windows-1254
-
windows-1255
-
windows-1256
-
windows-1257
-
windows-1258
-
windows-31j
-
ISO-2022-JP
-
ISO-2022-KR
-
ISO-8859-1
-
ISO-8859-2
-
ISO-8859-3
-
ISO-8859-4
-
ISO-8859-5
-
ISO-8859-6
-
ISO-8859-7
-
ISO-8859-8
-
ISO-8859-9
-
ISO-8859-13
-
ISO-8859-15
-
KOI8-R
-
UTF-8
-
UTF-16
-
UTF-16BE
-
UTF-16LE
-
EUC-JP
-
Shift_JIS
-
CP932
-
GB18030
-
GBK
-
ISCII91
-
EUC-KR
-
Big5
-
Big5-HKSCS
-
TIS-620)
-
-
1
cattr_accessor :available_settings
-
1
@@available_settings = YAML::load(File.open("#{Rails.root}/config/settings.yml"))
-
1
Redmine::Plugin.all.each do |plugin|
-
2
next unless plugin.settings
-
2
@@available_settings["plugin_#{plugin.id}"] = {'default' => plugin.settings[:default], 'serialized' => true}
-
end
-
-
1
validates_uniqueness_of :name
-
1
validates_inclusion_of :name, :in => @@available_settings.keys
-
558
validates_numericality_of :value, :only_integer => true, :if => Proc.new { |setting| @@available_settings[setting.name]['format'] == 'int' }
-
-
# Hash used to cache setting values
-
1
@cached_settings = {}
-
1
@cached_cleared_on = Time.now
-
-
1
def value
-
4058
v = read_attribute(:value)
-
# Unserialize serialized settings
-
4058
v = YAML::load(v) if @@available_settings[name]['serialized'] && v.is_a?(String)
-
4058
v = v.to_sym if @@available_settings[name]['format'] == 'symbol' && !v.blank?
-
4058
v
-
end
-
-
1
def value=(v)
-
3435
v = v.to_yaml if v && @@available_settings[name] && @@available_settings[name]['serialized']
-
3435
write_attribute(:value, v.to_s)
-
end
-
-
# Returns the value of the setting named name
-
1
def self.[](name)
-
58043
v = @cached_settings[name]
-
58043
v ? v : (@cached_settings[name] = find_or_default(name).value)
-
end
-
-
1
def self.[]=(name, v)
-
557
setting = find_or_default(name)
-
557
setting.value = (v ? v : "")
-
557
@cached_settings[name] = nil
-
557
setting.save
-
557
setting.value
-
end
-
-
# Defines getter and setter for each setting
-
# Then setting values can be read using: Setting.some_setting_name
-
# or set using Setting.some_setting_name = "some value"
-
1
@@available_settings.each do |name, params|
-
68
src = <<-END_SRC
-
def self.#{name}
-
self[:#{name}]
-
end
-
-
def self.#{name}?
-
self[:#{name}].to_i > 0
-
end
-
-
def self.#{name}=(value)
-
self[:#{name}] = value
-
end
-
END_SRC
-
68
class_eval src, __FILE__, __LINE__
-
end
-
-
# Helper that returns an array based on per_page_options setting
-
1
def self.per_page_options_array
-
132
per_page_options.split(%r{[\s,]}).collect(&:to_i).select {|n| n > 0}.sort
-
end
-
-
1
def self.openid?
-
246
Object.const_defined?(:OpenID) && self[:openid].to_i > 0
-
end
-
-
# Checks if settings have changed since the values were read
-
# and clears the cache hash if it's the case
-
# Called once per request
-
1
def self.check_cache
-
854
settings_updated_on = Setting.maximum(:updated_on)
-
854
if settings_updated_on && @cached_cleared_on <= settings_updated_on
-
132
clear_cache
-
end
-
end
-
-
# Clears the settings cache
-
1
def self.clear_cache
-
132
@cached_settings.clear
-
132
@cached_cleared_on = Time.now
-
132
logger.info "Settings cache cleared." if logger
-
end
-
-
1
private
-
# Returns the Setting instance for the setting named name
-
# (record found in database or new record with default value)
-
1
def self.find_or_default(name)
-
3890
name = name.to_s
-
3890
raise "There's no setting named #{name}" unless @@available_settings.has_key?(name)
-
3890
setting = find_by_name(name)
-
3890
unless setting
-
2878
setting = new(:name => name)
-
2878
setting.value = @@available_settings[name]['default']
-
end
-
3890
setting
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntry < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
# could have used polymorphic association
-
# project association here allows easy loading of time entries at project level with one database trip
-
1
belongs_to :project
-
1
belongs_to :issue
-
1
belongs_to :user
-
1
belongs_to :activity, :class_name => 'TimeEntryActivity', :foreign_key => 'activity_id'
-
-
1
attr_protected :project_id, :user_id, :tyear, :tmonth, :tweek
-
-
1
acts_as_customizable
-
acts_as_event :title => Proc.new {|o| "#{l_hours(o.hours)} (#{(o.issue || o.project).event_title})"},
-
:url => Proc.new {|o| {:controller => 'timelog', :action => 'index', :project_id => o.project, :issue_id => o.issue}},
-
:author => :user,
-
1
:description => :comments
-
-
acts_as_activity_provider :timestamp => "#{table_name}.created_on",
-
:author_key => :user_id,
-
1
:find_options => {:include => :project}
-
-
1
validates_presence_of :user_id, :activity_id, :project_id, :hours, :spent_on
-
1
validates_numericality_of :hours, :allow_nil => true, :message => :invalid
-
1
validates_length_of :comments, :maximum => 255, :allow_nil => true
-
1
before_validation :set_project_if_nil
-
1
validate :validate_time_entry
-
-
1
scope :visible, lambda {|*args| {
-
:include => :project,
-
:conditions => Project.allowed_to_condition(args.shift || User.current, :view_time_entries, *args)
-
71
}}
-
1
scope :on_issue, lambda {|issue| {
-
:include => :issue,
-
:conditions => "#{Issue.table_name}.root_id = #{issue.root_id} AND #{Issue.table_name}.lft >= #{issue.lft} AND #{Issue.table_name}.rgt <= #{issue.rgt}"
-
2
}}
-
1
scope :on_project, lambda {|project, include_subprojects| {
-
:include => :project,
-
:conditions => project.project_condition(include_subprojects)
-
2
}}
-
1
scope :spent_between, lambda {|from, to|
-
4
if from && to
-
{:conditions => ["#{TimeEntry.table_name}.spent_on BETWEEN ? AND ?", from, to]}
-
elsif from
-
{:conditions => ["#{TimeEntry.table_name}.spent_on >= ?", from]}
-
elsif to
-
{:conditions => ["#{TimeEntry.table_name}.spent_on <= ?", to]}
-
else
-
4
{}
-
end
-
}
-
-
1
safe_attributes 'hours', 'comments', 'issue_id', 'activity_id', 'spent_on', 'custom_field_values'
-
-
1
def initialize(attributes=nil, *args)
-
6
super
-
6
if new_record? && self.activity.nil?
-
6
if default_activity = TimeEntryActivity.default
-
6
self.activity_id = default_activity.id
-
end
-
6
self.hours = nil if hours == 0
-
end
-
end
-
-
1
def set_project_if_nil
-
3
self.project = issue.project if issue && project.nil?
-
end
-
-
1
def validate_time_entry
-
3
errors.add :hours, :invalid if hours && (hours < 0 || hours >= 1000)
-
3
errors.add :project_id, :invalid if project.nil?
-
3
errors.add :issue_id, :invalid if (issue_id && !issue) || (issue && project!=issue.project)
-
end
-
-
1
def hours=(h)
-
2
write_attribute :hours, (h.is_a?(String) ? (h.to_hours || h) : h)
-
end
-
-
1
def hours
-
27
h = read_attribute(:hours)
-
27
if h.is_a?(Float)
-
21
h.round(2)
-
else
-
6
h
-
end
-
end
-
-
# tyear, tmonth, tweek assigned where setting spent_on attributes
-
# these attributes make time aggregations easier
-
1
def spent_on=(date)
-
6
super
-
6
if spent_on.is_a?(Time)
-
self.spent_on = spent_on.to_date
-
end
-
6
self.tyear = spent_on ? spent_on.year : nil
-
6
self.tmonth = spent_on ? spent_on.month : nil
-
6
self.tweek = spent_on ? Date.civil(spent_on.year, spent_on.month, spent_on.day).cweek : nil
-
end
-
-
# Returns true if the time entry can be edited by usr, otherwise false
-
1
def editable_by?(usr)
-
4
(usr == user && usr.allowed_to?(:edit_own_time_entries, project)) || usr.allowed_to?(:edit_time_entries, project)
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntryActivity < Enumeration
-
1
has_many :time_entries, :foreign_key => 'activity_id'
-
-
1
OptionName = :enumeration_activities
-
-
1
def option_name
-
OptionName
-
end
-
-
1
def objects_count
-
time_entries.count
-
end
-
-
1
def transfer_relations(to)
-
time_entries.update_all("activity_id = #{to.id}")
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntryActivityCustomField < CustomField
-
1
def type_name
-
:enumeration_activities
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class TimeEntryCustomField < CustomField
-
1
def type_name
-
:label_spent_time
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Token < ActiveRecord::Base
-
1
belongs_to :user
-
1
validates_uniqueness_of :value
-
-
1
before_create :delete_previous_tokens, :generate_new_token
-
-
1
@@validity_time = 1.day
-
-
1
def generate_new_token
-
131
self.value = Token.generate_token_value
-
end
-
-
# Return true if token has expired
-
1
def expired?
-
return Time.now > self.created_on + @@validity_time
-
end
-
-
# Delete all expired tokens
-
1
def self.destroy_expired
-
Token.delete_all ["action NOT IN (?) AND created_on < ?", ['feeds', 'api'], Time.now - @@validity_time]
-
end
-
-
1
private
-
1
def self.generate_token_value
-
131
Redmine::Utils.random_hex(20)
-
end
-
-
# Removes obsolete tokens (same user and action)
-
1
def delete_previous_tokens
-
131
if user
-
131
Token.delete_all(['user_id = ? AND action = ?', user.id, action])
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Tracker < ActiveRecord::Base
-
1
before_destroy :check_integrity
-
1
has_many :issues
-
1
has_many :workflows, :dependent => :delete_all do
-
1
def copy(source_tracker)
-
476
Workflow.copy(source_tracker, nil, proxy_association.owner, nil)
-
end
-
end
-
-
1
has_and_belongs_to_many :projects
-
1
has_and_belongs_to_many :custom_fields, :class_name => 'IssueCustomField', :join_table => "#{table_name_prefix}custom_fields_trackers#{table_name_suffix}", :association_foreign_key => 'custom_field_id'
-
1
acts_as_list
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name
-
1
validates_length_of :name, :maximum => 30
-
-
1
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
-
-
1211
def to_s; name end
-
-
1
def <=>(tracker)
-
position <=> tracker.position
-
end
-
-
1
def self.all
-
46
find(:all, :order => 'position')
-
end
-
-
# Returns an array of IssueStatus that are used
-
# in the tracker's workflows
-
1
def issue_statuses
-
165
if @issue_statuses
-
84
return @issue_statuses
-
elsif new_record?
-
return []
-
end
-
-
81
ids = Workflow.
-
connection.select_rows("SELECT DISTINCT old_status_id, new_status_id FROM #{Workflow.table_name} WHERE tracker_id = #{id}").
-
flatten.
-
uniq
-
-
81
@issue_statuses = IssueStatus.find_all_by_id(ids).sort
-
end
-
-
1
private
-
1
def check_integrity
-
raise "Can't delete tracker" if Issue.find(:first, :conditions => ["tracker_id=?", self.id])
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require "digest/sha1"
-
-
1
class User < Principal
-
1
include Redmine::SafeAttributes
-
-
# Account statuses
-
1
STATUS_ANONYMOUS = 0
-
1
STATUS_ACTIVE = 1
-
1
STATUS_REGISTERED = 2
-
1
STATUS_LOCKED = 3
-
-
# Different ways of displaying/sorting users
-
1
USER_FORMATS = {
-
:firstname_lastname => {:string => '#{firstname} #{lastname}', :order => %w(firstname lastname id)},
-
:firstname => {:string => '#{firstname}', :order => %w(firstname id)},
-
:lastname_firstname => {:string => '#{lastname} #{firstname}', :order => %w(lastname firstname id)},
-
:lastname_coma_firstname => {:string => '#{lastname}, #{firstname}', :order => %w(lastname firstname id)},
-
:username => {:string => '#{login}', :order => %w(login id)},
-
}
-
-
1
MAIL_NOTIFICATION_OPTIONS = [
-
['all', :label_user_mail_option_all],
-
['selected', :label_user_mail_option_selected],
-
['only_my_events', :label_user_mail_option_only_my_events],
-
['only_assigned', :label_user_mail_option_only_assigned],
-
['only_owner', :label_user_mail_option_only_owner],
-
['none', :label_user_mail_option_none]
-
]
-
-
1
has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
-
:after_remove => Proc.new {|user, group| group.user_removed(user)}
-
1
has_many :changesets, :dependent => :nullify
-
1
has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
-
1
has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
-
1
has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
-
1
belongs_to :auth_source
-
-
# Active non-anonymous users scope
-
1
scope :active, :conditions => "#{User.table_name}.status = #{STATUS_ACTIVE}"
-
1
scope :logged, :conditions => "#{User.table_name}.status <> #{STATUS_ANONYMOUS}"
-
1
scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} }
-
-
1
acts_as_customizable
-
-
1
attr_accessor :password, :password_confirmation
-
1
attr_accessor :last_before_login_on
-
# Prevents unauthorized assignments
-
1
attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
-
-
1
LOGIN_LENGTH_LIMIT = 60
-
1
MAIL_LENGTH_LIMIT = 60
-
-
1
validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
-
1
validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
-
1
validates_uniqueness_of :mail, :if => Proc.new { |user| !user.mail.blank? }, :case_sensitive => false
-
# Login must contain lettres, numbers, underscores only
-
1
validates_format_of :login, :with => /^[a-z0-9_\-@\.]*$/i
-
1
validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
-
1
validates_length_of :firstname, :lastname, :maximum => 30
-
1
validates_format_of :mail, :with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i, :allow_blank => true
-
1
validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
-
1
validates_confirmation_of :password, :allow_nil => true
-
1
validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
-
1
validate :validate_password_length
-
-
1
before_create :set_mail_notification
-
1
before_save :update_hashed_password
-
1
before_destroy :remove_references_before_destroy
-
-
1
scope :in_group, lambda {|group|
-
group_id = group.is_a?(Group) ? group.id : group.to_i
-
{ :conditions => ["#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
-
}
-
1
scope :not_in_group, lambda {|group|
-
group_id = group.is_a?(Group) ? group.id : group.to_i
-
{ :conditions => ["#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id] }
-
}
-
-
1
def set_mail_notification
-
self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
-
true
-
end
-
-
1
def update_hashed_password
-
# update hashed_password if password was set
-
123
if self.password && self.auth_source_id.blank?
-
salt_password(password)
-
end
-
end
-
-
1
def reload(*args)
-
2
@name = nil
-
2
@projects_by_role = nil
-
2
super
-
end
-
-
1
def mail=(arg)
-
write_attribute(:mail, arg.to_s.strip)
-
end
-
-
1
def identity_url=(url)
-
if url.blank?
-
write_attribute(:identity_url, '')
-
else
-
begin
-
write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
-
rescue OpenIdAuthentication::InvalidOpenId
-
# Invlaid url, don't save
-
end
-
end
-
self.read_attribute(:identity_url)
-
end
-
-
# Returns the user that matches provided login and password, or nil
-
1
def self.try_to_login(login, password)
-
# Make sure no one can sign in with an empty password
-
123
return nil if password.to_s.empty?
-
123
user = find_by_login(login)
-
123
if user
-
# user is already in local database
-
123
return nil if !user.active?
-
123
if user.auth_source
-
# user has an external authentication method
-
return nil unless user.auth_source.authenticate(login, password)
-
else
-
# authentication with local password
-
123
return nil unless user.check_password?(password)
-
end
-
else
-
# user is not yet registered, try to authenticate with available sources
-
attrs = AuthSource.authenticate(login, password)
-
if attrs
-
user = new(attrs)
-
user.login = login
-
user.language = Setting.default_language
-
if user.save
-
user.reload
-
logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
-
end
-
end
-
end
-
123
user.update_attribute(:last_login_on, Time.now) if user && !user.new_record?
-
123
user
-
rescue => text
-
raise text
-
end
-
-
# Returns the user who matches the given autologin +key+ or nil
-
1
def self.try_to_autologin(key)
-
tokens = Token.find_all_by_action_and_value('autologin', key)
-
# Make sure there's only 1 token that matches the key
-
if tokens.size == 1
-
token = tokens.first
-
if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
-
token.user.update_attribute(:last_login_on, Time.now)
-
token.user
-
end
-
end
-
end
-
-
1
def self.name_formatter(formatter = nil)
-
6079
USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
-
end
-
-
# Returns an array of fields names than can be used to make an order statement for users
-
# according to how user names are displayed
-
# Examples:
-
#
-
# User.fields_for_order_statement => ['users.login', 'users.id']
-
# User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
-
1
def self.fields_for_order_statement(table=nil)
-
40
table ||= table_name
-
160
name_formatter[:order].map {|field| "#{table}.#{field}"}
-
end
-
-
# Return user's full name for display
-
1
def name(formatter = nil)
-
6039
f = self.class.name_formatter(formatter)
-
6039
if formatter
-
232
eval('"' + f[:string] + '"')
-
else
-
5807
@name ||= eval('"' + f[:string] + '"')
-
end
-
end
-
-
1
def active?
-
1709
self.status == STATUS_ACTIVE
-
end
-
-
1
def registered?
-
self.status == STATUS_REGISTERED
-
end
-
-
1
def locked?
-
self.status == STATUS_LOCKED
-
end
-
-
1
def activate
-
self.status = STATUS_ACTIVE
-
end
-
-
1
def register
-
self.status = STATUS_REGISTERED
-
end
-
-
1
def lock
-
self.status = STATUS_LOCKED
-
end
-
-
1
def activate!
-
update_attribute(:status, STATUS_ACTIVE)
-
end
-
-
1
def register!
-
update_attribute(:status, STATUS_REGISTERED)
-
end
-
-
1
def lock!
-
update_attribute(:status, STATUS_LOCKED)
-
end
-
-
# Returns true if +clear_password+ is the correct user's password, otherwise false
-
1
def check_password?(clear_password)
-
123
if auth_source_id.present?
-
auth_source.authenticate(self.login, clear_password)
-
else
-
123
User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
-
end
-
end
-
-
# Generates a random salt and computes hashed_password for +clear_password+
-
# The hashed password is stored in the following form: SHA1(salt + SHA1(password))
-
1
def salt_password(clear_password)
-
self.salt = User.generate_salt
-
self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
-
end
-
-
# Does the backend storage allow this user to change their password?
-
1
def change_password_allowed?
-
return true if auth_source.nil?
-
return auth_source.allow_password_changes?
-
end
-
-
# Generate and set a random password. Useful for automated user creation
-
# Based on Token#generate_token_value
-
#
-
1
def random_password
-
chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
-
password = ''
-
40.times { |i| password << chars[rand(chars.size-1)] }
-
self.password = password
-
self.password_confirmation = password
-
self
-
end
-
-
1
def pref
-
2482
self.preference ||= UserPreference.new(:user => self)
-
end
-
-
1
def time_zone
-
532
@time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
-
end
-
-
1
def wants_comments_in_reverse_order?
-
4
self.pref[:comments_sorting] == 'desc'
-
end
-
-
# Return user's RSS key (a 40 chars long string), used to access feeds
-
1
def rss_key
-
437
if rss_token.nil?
-
121
create_rss_token(:action => 'feeds')
-
end
-
437
rss_token.value
-
end
-
-
# Return user's API key (a 40 chars long string), used to access the API
-
1
def api_key
-
16
if api_token.nil?
-
10
create_api_token(:action => 'api')
-
end
-
16
api_token.value
-
end
-
-
# Return an array of project ids for which the user has explicitly turned mail notifications on
-
1
def notified_projects_ids
-
@notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
-
end
-
-
1
def notified_project_ids=(ids)
-
Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
-
Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
-
@notified_projects_ids = nil
-
notified_projects_ids
-
end
-
-
1
def valid_notification_options
-
self.class.valid_notification_options(self)
-
end
-
-
# Only users that belong to more than 1 project can select projects for which they are notified
-
1
def self.valid_notification_options(user=nil)
-
# Note that @user.membership.size would fail since AR ignores
-
# :include association option when doing a count
-
if user.nil? || user.memberships.length < 1
-
MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
-
else
-
MAIL_NOTIFICATION_OPTIONS
-
end
-
end
-
-
# Find a user account by matching the exact login and then a case-insensitive
-
# version. Exact matches will be given priority.
-
1
def self.find_by_login(login)
-
# First look for an exact match
-
246
user = all(:conditions => {:login => login}).detect {|u| u.login == login}
-
123
unless user
-
# Fail over to case-insensitive if none was found
-
user = first(:conditions => ["LOWER(login) = ?", login.to_s.downcase])
-
end
-
123
user
-
end
-
-
1
def self.find_by_rss_key(key)
-
token = Token.find_by_value(key)
-
token && token.user.active? ? token.user : nil
-
end
-
-
1
def self.find_by_api_key(key)
-
2
token = Token.find_by_action_and_value('api', key)
-
2
token && token.user.active? ? token.user : nil
-
end
-
-
# Makes find_by_mail case-insensitive
-
1
def self.find_by_mail(mail)
-
find(:first, :conditions => ["LOWER(mail) = ?", mail.to_s.downcase])
-
end
-
-
# Returns true if the default admin account can no longer be used
-
1
def self.default_admin_account_changed?
-
!User.active.find_by_login("admin").try(:check_password?, "admin")
-
end
-
-
1
def to_s
-
5066
name
-
end
-
-
# Returns the current day according to user's time zone
-
1
def today
-
4
if time_zone.nil?
-
4
Date.today
-
else
-
Time.now.in_time_zone(time_zone).to_date
-
end
-
end
-
-
# Returns the day of +time+ according to user's time zone
-
1
def time_to_date(time)
-
102
if time_zone.nil?
-
102
time.to_date
-
else
-
time.in_time_zone(time_zone).to_date
-
end
-
end
-
-
1
def logged?
-
15036
true
-
end
-
-
1
def anonymous?
-
!logged?
-
end
-
-
# Return user's roles for project
-
1
def roles_for_project(project)
-
6350
roles = []
-
# No role on archived projects
-
6350
return roles unless project && project.active?
-
6322
if logged?
-
# Find project membership
-
25343
membership = memberships.detect {|m| m.project_id == project.id}
-
6316
if membership
-
6250
roles = membership.roles
-
else
-
66
@role_non_member ||= Role.non_member
-
66
roles << @role_non_member
-
end
-
else
-
6
@role_anonymous ||= Role.anonymous
-
6
roles << @role_anonymous
-
end
-
6322
roles
-
end
-
-
# Return true if the user is a member of project
-
1
def member_of?(project)
-
!roles_for_project(project).detect {|role| role.member?}.nil?
-
end
-
-
# Returns a hash of user's projects grouped by roles
-
1
def projects_by_role
-
899
return @projects_by_role if @projects_by_role
-
-
795
@projects_by_role = Hash.new {|h,k| h[k]=[]}
-
265
memberships.each do |membership|
-
1040
membership.roles.each do |role|
-
1040
@projects_by_role[role] << membership.project if membership.project
-
end
-
end
-
265
@projects_by_role.each do |role, projects|
-
530
projects.uniq!
-
end
-
-
265
@projects_by_role
-
end
-
-
# Returns true if user is arg or belongs to arg
-
1
def is_or_belongs_to?(arg)
-
if arg.is_a?(User)
-
self == arg
-
elsif arg.is_a?(Group)
-
arg.users.include?(self)
-
else
-
false
-
end
-
end
-
-
# Return true if the user is allowed to do the specified action on a specific context
-
# Action can be:
-
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
-
# * a permission Symbol (eg. :edit_project)
-
# Context can be:
-
# * a project : returns true if user is allowed to do the specified action on this project
-
# * an array of projects : returns true if user is allowed on every project
-
# * nil with options[:global] set : check if user has at least one role allowed for this action,
-
# or falls back to Non Member / Anonymous permissions depending if the user is logged
-
1
def allowed_to?(action, context, options={}, &block)
-
6297
if context && context.is_a?(Project)
-
# No action allowed on archived projects
-
6045
return false unless context.active?
-
# No action allowed on disabled modules
-
6045
return false unless context.allows_to?(action)
-
# Admin users are authorized for anything else
-
5910
return true if admin?
-
-
5897
roles = roles_for_project(context)
-
5897
return false unless roles
-
5897
roles.detect {|role|
-
5897
(context.is_public? || role.member?) &&
-
5897
role.allowed_to?(action) &&
-
5602
(block_given? ? yield(role, self) : true)
-
}
-
252
elsif context && context.is_a?(Array)
-
# Authorize if user is authorized on every element of the array
-
context.map do |project|
-
allowed_to?(action, project, options, &block)
-
end.inject do |memo,allowed|
-
memo && allowed
-
end
-
252
elsif options[:global]
-
# Admin users are always authorized
-
252
return true if admin?
-
-
# authorize if user has at least one role that has this permission
-
627
roles = memberships.collect {|m| m.roles}.flatten.uniq
-
252
roles << (self.logged? ? Role.non_member : Role.anonymous)
-
252
roles.detect {|role|
-
role.allowed_to?(action) &&
-
461
(block_given? ? yield(role, self) : true)
-
}
-
else
-
false
-
end
-
end
-
-
# Is the user allowed to do the specified action on any project?
-
# See allowed_to? for the actions and valid options.
-
1
def allowed_to_globally?(action, options, &block)
-
allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
-
end
-
-
# Returns true if the user is allowed to delete his own account
-
1
def own_account_deletable?
-
Setting.unsubscribe? &&
-
(!admin? || User.active.first(:conditions => ["admin = ? AND id <> ?", true, id]).present?)
-
end
-
-
1
safe_attributes 'login',
-
'firstname',
-
'lastname',
-
'mail',
-
'mail_notification',
-
'language',
-
'custom_field_values',
-
'custom_fields',
-
'identity_url'
-
-
1
safe_attributes 'status',
-
'auth_source_id',
-
:if => lambda {|user, current_user| current_user.admin?}
-
-
1
safe_attributes 'group_ids',
-
:if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
-
-
# Utility method to help check if a user should be notified about an
-
# event.
-
#
-
# TODO: only supports Issue events currently
-
1
def notify_about?(object)
-
1112
case mail_notification
-
when 'all'
-
1112
true
-
when 'selected'
-
# user receives notifications for created/assigned issues on unselected projects
-
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
-
true
-
else
-
false
-
end
-
when 'none'
-
false
-
when 'only_my_events'
-
if object.is_a?(Issue) && (object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
-
true
-
else
-
false
-
end
-
when 'only_assigned'
-
if object.is_a?(Issue) && (is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was))
-
true
-
else
-
false
-
end
-
when 'only_owner'
-
if object.is_a?(Issue) && object.author == self
-
true
-
else
-
false
-
end
-
else
-
false
-
end
-
end
-
-
1
def self.current=(user)
-
981
@current_user = user
-
end
-
-
1
def self.current
-
22374
@current_user ||= User.anonymous
-
end
-
-
# Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
-
# one anonymous user per database.
-
1
def self.anonymous
-
288
anonymous_user = AnonymousUser.find(:first)
-
288
if anonymous_user.nil?
-
anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
-
raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
-
end
-
288
anonymous_user
-
end
-
-
# Salts all existing unsalted passwords
-
# It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
-
# This method is used in the SaltPasswords migration and is to be kept as is
-
1
def self.salt_unsalted_passwords!
-
transaction do
-
User.find_each(:conditions => "salt IS NULL OR salt = ''") do |user|
-
next if user.hashed_password.blank?
-
salt = User.generate_salt
-
hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
-
User.update_all("salt = '#{salt}', hashed_password = '#{hashed_password}'", ["id = ?", user.id] )
-
end
-
end
-
end
-
-
1
protected
-
-
1
def validate_password_length
-
# Password length validation based on setting
-
if !password.nil? && password.size < Setting.password_min_length.to_i
-
errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
-
end
-
end
-
-
1
private
-
-
# Removes references that are not handled by associations
-
# Things that are not deleted are reassociated with the anonymous user
-
1
def remove_references_before_destroy
-
return if self.id.nil?
-
-
substitute = User.anonymous
-
Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
-
Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
-
JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
-
JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
-
Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
# Remove private queries and keep public ones
-
::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
-
::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
-
TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
-
Token.delete_all ['user_id = ?', id]
-
Watcher.delete_all ['user_id = ?', id]
-
WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
-
end
-
-
# Return password digest
-
1
def self.hash_password(clear_password)
-
246
Digest::SHA1.hexdigest(clear_password || "")
-
end
-
-
# Returns a 128bits random salt as a hex string (32 chars long)
-
1
def self.generate_salt
-
Redmine::Utils.random_hex(16)
-
end
-
-
end
-
-
1
class AnonymousUser < User
-
1
validate :validate_anonymous_uniqueness, :on => :create
-
-
1
def validate_anonymous_uniqueness
-
# There should be only one AnonymousUser in the database
-
errors.add :base, 'An anonymous user already exists.' if AnonymousUser.find(:first)
-
end
-
-
1
def available_custom_fields
-
[]
-
end
-
-
# Overrides a few properties
-
1750
def logged?; false end
-
1
def admin; false end
-
1
def name(*args); I18n.t(:label_user_anonymous) end
-
1
def mail; nil end
-
1
def time_zone; nil end
-
1
def rss_key; nil end
-
-
# Anonymous user can not be destroyed
-
1
def destroy
-
false
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class UserCustomField < CustomField
-
1
def type_name
-
:label_user_plural
-
end
-
end
-
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class UserPreference < ActiveRecord::Base
-
1
belongs_to :user
-
1
serialize :others
-
-
1
attr_protected :others, :user_id
-
-
1
before_save :set_others_hash
-
-
1
def initialize(attributes=nil, *args)
-
242
super
-
242
self.others ||= {}
-
end
-
-
1
def set_others_hash
-
330
self.others ||= {}
-
end
-
-
1
def [](attr_name)
-
2610
if attribute_present? attr_name
-
484
super
-
else
-
2126
others ? others[attr_name] : nil
-
end
-
end
-
-
1
def []=(attr_name, value)
-
814
if attribute_present? attr_name
-
726
super
-
else
-
88
h = read_attribute(:others).dup || {}
-
88
h.update(attr_name => value)
-
88
write_attribute(:others, h)
-
88
value
-
end
-
end
-
-
1
def comments_sorting; self[:comments_sorting] end
-
1
def comments_sorting=(order); self[:comments_sorting]=order end
-
-
360
def warn_on_leaving_unsaved; self[:warn_on_leaving_unsaved] || '1'; end
-
1
def warn_on_leaving_unsaved=(value); self[:warn_on_leaving_unsaved]=value; end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Version < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
after_update :update_issues_from_sharing_change
-
1
belongs_to :project
-
1
has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
-
1
acts_as_customizable
-
acts_as_attachable :view_permission => :view_files,
-
1
:delete_permission => :manage_files
-
-
1
VERSION_STATUSES = %w(open locked closed)
-
1
VERSION_SHARINGS = %w(none descendants hierarchy tree system)
-
-
1
validates_presence_of :name
-
1
validates_uniqueness_of :name, :scope => [:project_id]
-
1
validates_length_of :name, :maximum => 60
-
1
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
-
1
validates_inclusion_of :status, :in => VERSION_STATUSES
-
1
validates_inclusion_of :sharing, :in => VERSION_SHARINGS
-
-
1
scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
-
1
scope :open, :conditions => {:status => 'open'}
-
1
scope :visible, lambda {|*args| { :include => :project,
-
:conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
-
-
1
safe_attributes 'name',
-
'description',
-
'effective_date',
-
'due_date',
-
'wiki_page_title',
-
'status',
-
'sharing',
-
'custom_field_values'
-
-
# Returns true if +user+ or current user is allowed to view the version
-
1
def visible?(user=User.current)
-
10
user.allowed_to?(:view_issues, self.project)
-
end
-
-
# Version files have same visibility as project files
-
1
def attachments_visible?(*args)
-
project.present? && project.attachments_visible?(*args)
-
end
-
-
1
def start_date
-
@start_date ||= fixed_issues.minimum('start_date')
-
end
-
-
1
def due_date
-
56
effective_date
-
end
-
-
1
def due_date=(arg)
-
self.effective_date=(arg)
-
end
-
-
# Returns the total estimated time for this version
-
# (sum of leaves estimated_hours)
-
1
def estimated_hours
-
@estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
-
end
-
-
# Returns the total reported time for this version
-
1
def spent_hours
-
@spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
-
end
-
-
1
def closed?
-
status == 'closed'
-
end
-
-
1
def open?
-
status == 'open'
-
end
-
-
# Returns true if the version is completed: due date reached and no open issues
-
1
def completed?
-
8
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
-
end
-
-
1
def behind_schedule?
-
if completed_pourcent == 100
-
return false
-
elsif due_date && start_date
-
done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
-
return done_date <= Date.today
-
else
-
false # No issues so it's not late
-
end
-
end
-
-
# Returns the completion percentage of this version based on the amount of open/closed issues
-
# and the time spent on the open issues.
-
1
def completed_pourcent
-
16
if issues_count == 0
-
0
-
16
elsif open_issues_count == 0
-
100
-
else
-
16
issues_progress(false) + issues_progress(true)
-
end
-
end
-
-
# Returns the percentage of issues that have been marked as 'closed'.
-
1
def closed_pourcent
-
8
if issues_count == 0
-
0
-
else
-
8
issues_progress(false)
-
end
-
end
-
-
# Returns true if the version is overdue: due date reached and some open issues
-
1
def overdue?
-
effective_date && (effective_date < Date.today) && (open_issues_count > 0)
-
end
-
-
# Returns assigned issues count
-
1
def issues_count
-
72
load_issue_counts
-
72
@issue_count
-
end
-
-
# Returns the total amount of open issues for this version.
-
1
def open_issues_count
-
40
load_issue_counts
-
40
@open_issues_count
-
end
-
-
# Returns the total amount of closed issues for this version.
-
1
def closed_issues_count
-
16
load_issue_counts
-
16
@closed_issues_count
-
end
-
-
1
def wiki_page
-
8
if project.wiki && !wiki_page_title.blank?
-
@wiki_page ||= project.wiki.find_page(wiki_page_title)
-
end
-
8
@wiki_page
-
end
-
-
1461
def to_s; name end
-
-
1
def to_s_with_project
-
"#{project} - #{name}"
-
end
-
-
# Versions are sorted by effective_date and "Project Name - Version name"
-
# Those with no effective_date are at the end, sorted by "Project Name - Version name"
-
1
def <=>(version)
-
3407
if self.effective_date
-
2471
if version.effective_date
-
2347
if self.effective_date == version.effective_date
-
1234
"#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
-
else
-
1113
self.effective_date <=> version.effective_date
-
end
-
else
-
124
-1
-
end
-
else
-
936
if version.effective_date
-
414
1
-
else
-
522
"#{self.project.name} - #{self.name}" <=> "#{version.project.name} - #{version.name}"
-
end
-
end
-
end
-
-
# Returns the sharings that +user+ can set the version to
-
1
def allowed_sharings(user = User.current)
-
VERSION_SHARINGS.select do |s|
-
if sharing == s
-
true
-
else
-
case s
-
when 'system'
-
# Only admin users can set a systemwide sharing
-
user.admin?
-
when 'hierarchy', 'tree'
-
# Only users allowed to manage versions of the root project can
-
# set sharing to hierarchy or tree
-
project.nil? || user.allowed_to?(:manage_versions, project.root)
-
else
-
true
-
end
-
end
-
end
-
end
-
-
1
private
-
-
1
def load_issue_counts
-
128
unless @issue_count
-
8
@open_issues_count = 0
-
8
@closed_issues_count = 0
-
8
fixed_issues.count(:all, :group => :status).each do |status, count|
-
8
if status.is_closed?
-
@closed_issues_count += count
-
else
-
8
@open_issues_count += count
-
end
-
end
-
8
@issue_count = @open_issues_count + @closed_issues_count
-
end
-
end
-
-
# Update the issue's fixed versions. Used if a version's sharing changes.
-
1
def update_issues_from_sharing_change
-
23
if sharing_changed?
-
if VERSION_SHARINGS.index(sharing_was).nil? ||
-
VERSION_SHARINGS.index(sharing).nil? ||
-
VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
-
Issue.update_versions_from_sharing_change self
-
end
-
end
-
end
-
-
# Returns the average estimated time of assigned issues
-
# or 1 if no issue has an estimated time
-
# Used to weigth unestimated issues in progress calculation
-
1
def estimated_average
-
32
if @estimated_average.nil?
-
8
average = fixed_issues.average(:estimated_hours).to_f
-
8
if average == 0
-
8
average = 1
-
end
-
8
@estimated_average = average
-
end
-
32
@estimated_average
-
end
-
-
# Returns the total progress of open or closed issues. The returned percentage takes into account
-
# the amount of estimated time set for this version.
-
#
-
# Examples:
-
# issues_progress(true) => returns the progress percentage for open issues.
-
# issues_progress(false) => returns the progress percentage for closed issues.
-
1
def issues_progress(open)
-
40
@issues_progress ||= {}
-
40
@issues_progress[open] ||= begin
-
16
progress = 0
-
16
if issues_count > 0
-
16
ratio = open ? 'done_ratio' : 100
-
-
16
done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
-
:joins => :status,
-
:conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
-
16
progress = done / (estimated_average * issues_count)
-
end
-
16
progress
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class VersionCustomField < CustomField
-
1
def type_name
-
:label_version_plural
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Watcher < ActiveRecord::Base
-
1
belongs_to :watchable, :polymorphic => true
-
1
belongs_to :user
-
-
1
validates_presence_of :user
-
1
validates_uniqueness_of :user_id, :scope => [:watchable_type, :watchable_id]
-
1
validate :validate_user
-
-
# Unwatch things that users are no longer allowed to view
-
1
def self.prune(options={})
-
if options.has_key?(:user)
-
prune_single_user(options[:user], options)
-
else
-
pruned = 0
-
User.find(:all, :conditions => "id IN (SELECT DISTINCT user_id FROM #{table_name})").each do |user|
-
pruned += prune_single_user(user, options)
-
end
-
pruned
-
end
-
end
-
-
1
protected
-
-
1
def validate_user
-
errors.add :user_id, :invalid unless user.nil? || user.active?
-
end
-
-
1
private
-
-
1
def self.prune_single_user(user, options={})
-
return unless user.is_a?(User)
-
pruned = 0
-
find(:all, :conditions => {:user_id => user.id}).each do |watcher|
-
next if watcher.watchable.nil?
-
-
if options.has_key?(:project)
-
next unless watcher.watchable.respond_to?(:project) && watcher.watchable.project == options[:project]
-
end
-
-
if watcher.watchable.respond_to?(:visible?)
-
unless watcher.watchable.visible?(user)
-
watcher.destroy
-
pruned += 1
-
end
-
end
-
end
-
pruned
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Wiki < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
1
belongs_to :project
-
1
has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
-
1
has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
-
-
1
acts_as_watchable
-
-
1
validates_presence_of :start_page
-
1
validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
-
-
1
safe_attributes 'start_page'
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
-
end
-
-
# Returns the wiki page that acts as the sidebar content
-
# or nil if no such page exists
-
1
def sidebar
-
2
@sidebar ||= find_page('Sidebar', :with_redirect => false)
-
end
-
-
# find the page with the given title
-
# if page doesn't exist, return a new page
-
1
def find_or_new_page(title)
-
title = start_page if title.blank?
-
find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
-
end
-
-
# find the page with the given title
-
1
def find_page(title, options = {})
-
10
@page_found_with_redirect = false
-
10
title = start_page if title.blank?
-
10
title = Wiki.titleize(title)
-
10
page = pages.first(:conditions => ["LOWER(title) = LOWER(?)", title])
-
10
if !page && !(options[:with_redirect] == false)
-
# search for a redirect
-
4
redirect = redirects.first(:conditions => ["LOWER(title) = LOWER(?)", title])
-
4
if redirect
-
page = find_page(redirect.redirects_to, :with_redirect => false)
-
@page_found_with_redirect = true
-
end
-
end
-
10
page
-
end
-
-
# Returns true if the last page was found with a redirect
-
1
def page_found_with_redirect?
-
@page_found_with_redirect
-
end
-
-
# Finds a page by title
-
# The given string can be of one of the forms: "title" or "project:title"
-
# Examples:
-
# Wiki.find_page("bar", project => foo)
-
# Wiki.find_page("foo:bar")
-
1
def self.find_page(title, options = {})
-
project = options[:project]
-
if title.to_s =~ %r{^([^\:]+)\:(.*)$}
-
project_identifier, title = $1, $2
-
project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
-
end
-
if project && project.wiki
-
page = project.wiki.find_page(title)
-
if page && page.content
-
page
-
end
-
end
-
end
-
-
# turn a string into a valid page title
-
1
def self.titleize(title)
-
# replace spaces with _ and remove unwanted caracters
-
30
title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
-
# upcase the first letter
-
30
title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
-
30
title
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'zlib'
-
-
1
class WikiContent < ActiveRecord::Base
-
1
self.locking_column = 'version'
-
1
belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
-
1
belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
-
1
validates_presence_of :text
-
1
validates_length_of :comments, :maximum => 255, :allow_nil => true
-
-
1
acts_as_versioned
-
-
1
def visible?(user=User.current)
-
page.visible?(user)
-
end
-
-
1
def project
-
page.project
-
end
-
-
1
def attachments
-
page.nil? ? [] : page.attachments
-
end
-
-
# Returns the mail adresses of users that should be notified
-
1
def recipients
-
notified = project.notified_users
-
notified.reject! {|user| !visible?(user)}
-
notified.collect(&:mail)
-
end
-
-
# Return true if the content is the current page content
-
1
def current_version?
-
true
-
end
-
-
1
class Version
-
1
belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
-
1
belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
-
1
attr_protected :data
-
-
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
-
:description => :comments,
-
:datetime => :updated_on,
-
:type => 'wiki-page',
-
1
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
-
-
acts_as_activity_provider :type => 'wiki_edits',
-
:timestamp => "#{WikiContent.versioned_table_name}.updated_on",
-
:author_key => "#{WikiContent.versioned_table_name}.author_id",
-
:permission => :view_wiki_edits,
-
:find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
-
"#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
-
"#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
-
"#{WikiContent.versioned_table_name}.id",
-
:joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
-
"LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
-
1
"LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
-
-
1
def text=(plain)
-
4
case Setting.wiki_compression
-
when 'gzip'
-
begin
-
self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
-
self.compression = 'gzip'
-
rescue
-
self.data = plain
-
self.compression = ''
-
end
-
else
-
4
self.data = plain
-
4
self.compression = ''
-
end
-
4
plain
-
end
-
-
1
def text
-
@text ||= begin
-
str = case compression
-
when 'gzip'
-
Zlib::Inflate.inflate(data)
-
else
-
# uncompressed data
-
data
-
end
-
str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
-
str
-
end
-
end
-
-
1
def project
-
page.project
-
end
-
-
# Return true if the content is the current page content
-
1
def current_version?
-
page.content.version == self.version
-
end
-
-
# Returns the previous version or nil
-
1
def previous
-
@previous ||= WikiContent::Version.find(:first,
-
:order => 'version DESC',
-
:include => :author,
-
:conditions => ["wiki_content_id = ? AND version < ?", wiki_content_id, version])
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WikiContentObserver < ActiveRecord::Observer
-
1
def after_create(wiki_content)
-
4
Mailer.wiki_content_added(wiki_content).deliver if Setting.notified_events.include?('wiki_content_added')
-
end
-
-
1
def after_update(wiki_content)
-
if wiki_content.text_changed?
-
Mailer.wiki_content_updated(wiki_content).deliver if Setting.notified_events.include?('wiki_content_updated')
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'diff'
-
1
require 'enumerator'
-
-
1
class WikiPage < ActiveRecord::Base
-
1
include Redmine::SafeAttributes
-
-
1
belongs_to :wiki
-
1
has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
-
1
acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
-
1
acts_as_tree :dependent => :nullify, :order => 'title'
-
-
1
acts_as_watchable
-
acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
-
:description => :text,
-
:datetime => :created_on,
-
1
:url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
-
-
acts_as_searchable :columns => ['title', "#{WikiContent.table_name}.text"],
-
:include => [{:wiki => :project}, :content],
-
:permission => :view_wiki_pages,
-
1
:project_key => "#{Wiki.table_name}.project_id"
-
-
1
attr_accessor :redirect_existing_links
-
-
1
validates_presence_of :title
-
1
validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
-
1
validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
-
1
validates_associated :content
-
-
1
validate :validate_parent_title
-
1
before_destroy :remove_redirects
-
1
before_save :handle_redirects
-
-
# eager load information about last updates, without loading text
-
1
scope :with_updated_on, {
-
:select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
-
:joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
-
}
-
-
# Wiki pages that are protected by default
-
1
DEFAULT_PROTECTED_PAGES = %w(sidebar)
-
-
1
safe_attributes 'parent_id',
-
:if => lambda {|page, user| page.new_record? || user.allowed_to?(:rename_wiki_pages, page.project)}
-
-
1
def initialize(attributes=nil, *args)
-
4
super
-
4
if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
-
self.protected = true
-
end
-
end
-
-
1
def visible?(user=User.current)
-
!user.nil? && user.allowed_to?(:view_wiki_pages, project)
-
end
-
-
1
def title=(value)
-
8
value = Wiki.titleize(value)
-
8
@previous_title = read_attribute(:title) if @previous_title.blank?
-
8
write_attribute(:title, value)
-
end
-
-
1
def handle_redirects
-
4
self.title = Wiki.titleize(title)
-
# Manage redirects if the title has changed
-
4
if !@previous_title.blank? && (@previous_title != title) && !new_record?
-
# Update redirects that point to the old title
-
wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
-
r.redirects_to = title
-
r.title == r.redirects_to ? r.destroy : r.save
-
end
-
# Remove redirects for the new title
-
wiki.redirects.find_all_by_title(title).each(&:destroy)
-
# Create a redirect to the new title
-
wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
-
@previous_title = nil
-
end
-
end
-
-
1
def remove_redirects
-
# Remove redirects to this page
-
wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
-
end
-
-
1
def pretty_title
-
18
WikiPage.pretty_title(title)
-
end
-
-
1
def content_for_version(version=nil)
-
result = content.versions.find_by_version(version.to_i) if version
-
result ||= content
-
result
-
end
-
-
1
def diff(version_to=nil, version_from=nil)
-
version_to = version_to ? version_to.to_i : self.content.version
-
version_from = version_from ? version_from.to_i : version_to - 1
-
version_to, version_from = version_from, version_to unless version_from < version_to
-
-
content_to = content.versions.find_by_version(version_to)
-
content_from = content.versions.find_by_version(version_from)
-
-
(content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
-
end
-
-
1
def annotate(version=nil)
-
version = version ? version.to_i : self.content.version
-
c = content.versions.find_by_version(version)
-
c ? WikiAnnotate.new(c) : nil
-
end
-
-
1
def self.pretty_title(str)
-
18
(str && str.is_a?(String)) ? str.tr('_', ' ') : str
-
end
-
-
1
def project
-
18
wiki.project
-
end
-
-
1
def text
-
2
content.text if content
-
end
-
-
1
def updated_on
-
36
unless @updated_on
-
18
if time = read_attribute(:updated_on)
-
# content updated_on was eager loaded with the page
-
18
begin
-
18
@updated_on = (self.class.default_timezone == :utc ? Time.parse(time.to_s).utc : Time.parse(time.to_s).localtime)
-
rescue
-
end
-
else
-
@updated_on = content && content.updated_on
-
end
-
end
-
36
@updated_on
-
end
-
-
# Returns true if usr is allowed to edit the page, otherwise false
-
1
def editable_by?(usr)
-
!protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
-
end
-
-
1
def attachments_deletable?(usr=User.current)
-
editable_by?(usr) && super(usr)
-
end
-
-
1
def parent_title
-
@parent_title || (self.parent && self.parent.pretty_title)
-
end
-
-
1
def parent_title=(t)
-
@parent_title = t
-
parent_page = t.blank? ? nil : self.wiki.find_page(t)
-
self.parent = parent_page
-
end
-
-
1
protected
-
-
1
def validate_parent_title
-
6
errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
-
6
errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
-
6
errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
-
end
-
end
-
-
1
class WikiDiff < Redmine::Helpers::Diff
-
1
attr_reader :content_to, :content_from
-
-
1
def initialize(content_to, content_from)
-
@content_to = content_to
-
@content_from = content_from
-
super(content_to.text, content_from.text)
-
end
-
end
-
-
1
class WikiAnnotate
-
1
attr_reader :lines, :content
-
-
1
def initialize(content)
-
@content = content
-
current = content
-
current_lines = current.text.split(/\r?\n/)
-
@lines = current_lines.collect {|t| [nil, nil, t]}
-
positions = []
-
current_lines.size.times {|i| positions << i}
-
while (current.previous)
-
d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
-
d.each_slice(3) do |s|
-
sign, line = s[0], s[1]
-
if sign == '+' && positions[line] && positions[line] != -1
-
if @lines[positions[line]][0].nil?
-
@lines[positions[line]][0] = current.version
-
@lines[positions[line]][1] = current.author
-
end
-
end
-
end
-
d.each_slice(3) do |s|
-
sign, line = s[0], s[1]
-
if sign == '-'
-
positions.insert(line, -1)
-
else
-
positions[line] = nil
-
end
-
end
-
positions.compact!
-
# Stop if every line is annotated
-
break unless @lines.detect { |line| line[0].nil? }
-
current = current.previous
-
end
-
@lines.each { |line|
-
line[0] ||= current.version
-
# if the last known version is > 1 (eg. history was cleared), we don't know the author
-
line[1] ||= current.author if current.version == 1
-
}
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class WikiRedirect < ActiveRecord::Base
-
1
belongs_to :wiki
-
-
1
validates_presence_of :title, :redirects_to
-
1
validates_length_of :title, :redirects_to, :maximum => 255
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class Workflow < ActiveRecord::Base
-
1
belongs_to :role
-
1
belongs_to :old_status, :class_name => 'IssueStatus', :foreign_key => 'old_status_id'
-
1
belongs_to :new_status, :class_name => 'IssueStatus', :foreign_key => 'new_status_id'
-
-
1
validates_presence_of :role, :old_status, :new_status
-
-
# Returns workflow transitions count by tracker and role
-
1
def self.count_by_tracker_and_role
-
counts = connection.select_all("SELECT role_id, tracker_id, count(id) AS c FROM #{Workflow.table_name} GROUP BY role_id, tracker_id")
-
roles = Role.find(:all, :order => 'builtin, position')
-
trackers = Tracker.find(:all, :order => 'position')
-
-
result = []
-
trackers.each do |tracker|
-
t = []
-
roles.each do |role|
-
row = counts.detect {|c| c['role_id'].to_s == role.id.to_s && c['tracker_id'].to_s == tracker.id.to_s}
-
t << [role, (row.nil? ? 0 : row['c'].to_i)]
-
end
-
result << [tracker, t]
-
end
-
-
result
-
end
-
-
# Copies workflows from source to targets
-
1
def self.copy(source_tracker, source_role, target_trackers, target_roles)
-
476
unless source_tracker.is_a?(Tracker) || source_role.is_a?(Role)
-
raise ArgumentError.new("source_tracker or source_role must be specified")
-
end
-
-
476
target_trackers = [target_trackers].flatten.compact
-
476
target_roles = [target_roles].flatten.compact
-
-
476
target_trackers = Tracker.all if target_trackers.empty?
-
476
target_roles = Role.all if target_roles.empty?
-
-
476
target_trackers.each do |target_tracker|
-
476
target_roles.each do |target_role|
-
2380
copy_one(source_tracker || target_tracker,
-
source_role || target_role,
-
target_tracker,
-
target_role)
-
end
-
end
-
end
-
-
# Copies a single set of workflows from source to target
-
1
def self.copy_one(source_tracker, source_role, target_tracker, target_role)
-
2380
unless source_tracker.is_a?(Tracker) && !source_tracker.new_record? &&
-
source_role.is_a?(Role) && !source_role.new_record? &&
-
target_tracker.is_a?(Tracker) && !target_tracker.new_record? &&
-
target_role.is_a?(Role) && !target_role.new_record?
-
-
raise ArgumentError.new("arguments can not be nil or unsaved objects")
-
end
-
-
2380
if source_tracker == target_tracker && source_role == target_role
-
false
-
else
-
2380
transaction do
-
2380
delete_all :tracker_id => target_tracker.id, :role_id => target_role.id
-
2380
connection.insert "INSERT INTO #{Workflow.table_name} (tracker_id, role_id, old_status_id, new_status_id, author, assignee)" +
-
" SELECT #{target_tracker.id}, #{target_role.id}, old_status_id, new_status_id, author, assignee" +
-
" FROM #{Workflow.table_name}" +
-
" WHERE tracker_id = #{source_tracker.id} AND role_id = #{source_role.id}"
-
end
-
2380
true
-
end
-
end
-
end
-
1
require 'rexml/document'
-
1
require 'SVG/Graph/Graph'
-
1
require 'SVG/Graph/BarBase'
-
-
1
module SVG
-
1
module Graph
-
# === Create presentation quality SVG bar graphs easily
-
#
-
# = Synopsis
-
#
-
# require 'SVG/Graph/Bar'
-
#
-
# fields = %w(Jan Feb Mar);
-
# data_sales_02 = [12, 45, 21]
-
#
-
# graph = SVG::Graph::Bar.new(
-
# :height => 500,
-
# :width => 300,
-
# :fields => fields
-
# )
-
#
-
# graph.add_data(
-
# :data => data_sales_02,
-
# :title => 'Sales 2002'
-
# )
-
#
-
# print "Content-type: image/svg+xml\r\n\r\n"
-
# print graph.burn
-
#
-
# = Description
-
#
-
# This object aims to allow you to easily create high quality
-
# SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
-
# style sheet or supply your own. Either way there are many options which
-
# can be configured to give you control over how the graph is generated -
-
# with or without a key, data elements at each point, title, subtitle etc.
-
#
-
# = Notes
-
#
-
# The default stylesheet handles upto 12 data sets, if you
-
# use more you must create your own stylesheet and add the
-
# additional settings for the extra data sets. You will know
-
# if you go over 12 data sets as they will have no style and
-
# be in black.
-
#
-
# = Examples
-
#
-
# * http://germane-software.com/repositories/public/SVG/test/test.rb
-
#
-
# = See also
-
#
-
# * SVG::Graph::Graph
-
# * SVG::Graph::BarHorizontal
-
# * SVG::Graph::Line
-
# * SVG::Graph::Pie
-
# * SVG::Graph::Plot
-
# * SVG::Graph::TimeSeries
-
1
class Bar < BarBase
-
1
include REXML
-
-
# See Graph::initialize and BarBase::set_defaults
-
1
def set_defaults
-
super
-
self.top_align = self.top_font = 1
-
end
-
-
1
protected
-
-
1
def get_x_labels
-
@config[:fields]
-
end
-
-
1
def get_y_labels
-
maxvalue = max_value
-
minvalue = min_value
-
range = maxvalue - minvalue
-
-
top_pad = range == 0 ? 10 : range / 20.0
-
scale_range = (maxvalue + top_pad) - minvalue
-
-
scale_division = scale_divisions || (scale_range / 10.0)
-
-
if scale_integers
-
scale_division = scale_division < 1 ? 1 : scale_division.round
-
end
-
-
rv = []
-
maxvalue = maxvalue%scale_division == 0 ?
-
maxvalue : maxvalue + scale_division
-
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
-
return rv
-
end
-
-
1
def x_label_offset( width )
-
width / 2.0
-
end
-
-
1
def draw_data
-
minvalue = min_value
-
fieldwidth = field_width
-
-
unit_size = (@graph_height.to_f - font_size*2*top_font) /
-
(get_y_labels.max - get_y_labels.min)
-
bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
-
-
bar_width = fieldwidth - bargap
-
bar_width /= @data.length if stack == :side
-
x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
-
-
bottom = @graph_height
-
-
field_count = 0
-
@config[:fields].each_index { |i|
-
dataset_count = 0
-
for dataset in @data
-
-
# cases (assume 0 = +ve):
-
# value min length
-
# +ve +ve value - min
-
# +ve -ve value - 0
-
# -ve -ve value.abs - 0
-
-
value = dataset[:data][i]
-
-
left = (fieldwidth * field_count)
-
-
length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
-
# top is 0 if value is negative
-
top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)
-
left += bar_width * dataset_count if stack == :side
-
-
@graph.add_element( "rect", {
-
"x" => left.to_s,
-
"y" => top.to_s,
-
"width" => bar_width.to_s,
-
"height" => length.to_s,
-
"class" => "fill#{dataset_count+1}"
-
})
-
-
make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
-
dataset_count += 1
-
end
-
field_count += 1
-
}
-
end
-
end
-
end
-
end
-
1
require 'rexml/document'
-
1
require 'SVG/Graph/Graph'
-
-
1
module SVG
-
1
module Graph
-
# = Synopsis
-
#
-
# A superclass for bar-style graphs. Do not attempt to instantiate
-
# directly; use one of the subclasses instead.
-
#
-
# = Author
-
#
-
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
-
#
-
# Copyright 2004 Sean E. Russell
-
# This software is available under the Ruby license[LICENSE.txt]
-
#
-
1
class BarBase < SVG::Graph::Graph
-
# Ensures that :fields are provided in the configuration.
-
1
def initialize config
-
raise "fields was not supplied or is empty" unless config[:fields] &&
-
config[:fields].kind_of?(Array) &&
-
config[:fields].length > 0
-
super
-
end
-
-
# In addition to the defaults set in Graph::initialize, sets
-
# [bar_gap] true
-
# [stack] :overlap
-
1
def set_defaults
-
init_with( :bar_gap => true, :stack => :overlap )
-
end
-
-
# Whether to have a gap between the bars or not, default
-
# is true, set to false if you don't want gaps.
-
1
attr_accessor :bar_gap
-
# How to stack data sets. :overlap overlaps bars with
-
# transparent colors, :top stacks bars on top of one another,
-
# :side stacks the bars side-by-side. Defaults to :overlap.
-
1
attr_accessor :stack
-
-
-
1
protected
-
-
1
def max_value
-
@data.collect{|x| x[:data].max}.max
-
end
-
-
1
def min_value
-
min = 0
-
if min_scale_value.nil?
-
min = @data.collect{|x| x[:data].min}.min
-
min = min > 0 ? 0 : min
-
else
-
min = min_scale_value
-
end
-
return min
-
end
-
-
1
def get_css
-
return <<EOL
-
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
-
.key1,.fill1{
-
fill: #ff0000;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 0.5px;
-
}
-
.key2,.fill2{
-
fill: #0000ff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key3,.fill3{
-
fill: #00ff00;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key4,.fill4{
-
fill: #ffcc00;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key5,.fill5{
-
fill: #00ccff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key6,.fill6{
-
fill: #ff00ff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key7,.fill7{
-
fill: #00ffff;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key8,.fill8{
-
fill: #ffff00;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key9,.fill9{
-
fill: #cc6666;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key10,.fill10{
-
fill: #663399;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key11,.fill11{
-
fill: #339900;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
.key12,.fill12{
-
fill: #9966FF;
-
fill-opacity: 0.5;
-
stroke: none;
-
stroke-width: 1px;
-
}
-
EOL
-
end
-
end
-
end
-
end
-
1
require 'rexml/document'
-
1
require 'SVG/Graph/BarBase'
-
-
1
module SVG
-
1
module Graph
-
# === Create presentation quality SVG horitonzal bar graphs easily
-
#
-
# = Synopsis
-
#
-
# require 'SVG/Graph/BarHorizontal'
-
#
-
# fields = %w(Jan Feb Mar)
-
# data_sales_02 = [12, 45, 21]
-
#
-
# graph = SVG::Graph::BarHorizontal.new({
-
# :height => 500,
-
# :width => 300,
-
# :fields => fields,
-
# })
-
#
-
# graph.add_data({
-
# :data => data_sales_02,
-
# :title => 'Sales 2002',
-
# })
-
#
-
# print "Content-type: image/svg+xml\r\n\r\n"
-
# print graph.burn
-
#
-
# = Description
-
#
-
# This object aims to allow you to easily create high quality
-
# SVG horitonzal bar graphs. You can either use the default style sheet
-
# or supply your own. Either way there are many options which can
-
# be configured to give you control over how the graph is
-
# generated - with or without a key, data elements at each point,
-
# title, subtitle etc.
-
#
-
# = Examples
-
#
-
# * http://germane-software.com/repositories/public/SVG/test/test.rb
-
#
-
# = See also
-
#
-
# * SVG::Graph::Graph
-
# * SVG::Graph::Bar
-
# * SVG::Graph::Line
-
# * SVG::Graph::Pie
-
# * SVG::Graph::Plot
-
# * SVG::Graph::TimeSeries
-
#
-
# == Author
-
#
-
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
-
#
-
# Copyright 2004 Sean E. Russell
-
# This software is available under the Ruby license[LICENSE.txt]
-
#
-
1
class BarHorizontal < BarBase
-
# In addition to the defaults set in BarBase::set_defaults, sets
-
# [rotate_y_labels] true
-
# [show_x_guidelines] true
-
# [show_y_guidelines] false
-
1
def set_defaults
-
super
-
init_with(
-
:rotate_y_labels => true,
-
:show_x_guidelines => true,
-
:show_y_guidelines => false
-
)
-
self.right_align = self.right_font = 1
-
end
-
-
1
protected
-
-
1
def get_x_labels
-
maxvalue = max_value
-
minvalue = min_value
-
range = maxvalue - minvalue
-
top_pad = range == 0 ? 10 : range / 20.0
-
scale_range = (maxvalue + top_pad) - minvalue
-
-
scale_division = scale_divisions || (scale_range / 10.0)
-
-
if scale_integers
-
scale_division = scale_division < 1 ? 1 : scale_division.round
-
end
-
-
rv = []
-
maxvalue = maxvalue%scale_division == 0 ?
-
maxvalue : maxvalue + scale_division
-
minvalue.step( maxvalue, scale_division ) {|v| rv << v}
-
return rv
-
end
-
-
1
def get_y_labels
-
@config[:fields]
-
end
-
-
1
def y_label_offset( height )
-
height / -2.0
-
end
-
-
1
def draw_data
-
minvalue = min_value
-
fieldheight = field_height
-
-
unit_size = (@graph_width.to_f - font_size*2*right_font ) /
-
(get_x_labels.max - get_x_labels.min )
-
bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
-
-
bar_height = fieldheight - bargap
-
bar_height /= @data.length if stack == :side
-
y_mod = (bar_height / 2) + (font_size / 2)
-
-
field_count = 1
-
@config[:fields].each_index { |i|
-
dataset_count = 0
-
for dataset in @data
-
value = dataset[:data][i]
-
-
top = @graph_height - (fieldheight * field_count)
-
top += (bar_height * dataset_count) if stack == :side
-
# cases (assume 0 = +ve):
-
# value min length left
-
# +ve +ve value.abs - min minvalue.abs
-
# +ve -ve value.abs - 0 minvalue.abs
-
# -ve -ve value.abs - 0 minvalue.abs + value
-
length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
-
left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size
-
-
@graph.add_element( "rect", {
-
"x" => left.to_s,
-
"y" => top.to_s,
-
"width" => length.to_s,
-
"height" => bar_height.to_s,
-
"class" => "fill#{dataset_count+1}"
-
})
-
-
make_datapoint_text(
-
left+length+5, top+y_mod, value, "text-anchor: start; "
-
)
-
dataset_count += 1
-
end
-
field_count += 1
-
}
-
end
-
end
-
end
-
end
-
1
begin
-
1
require 'zlib'
-
1
@@__have_zlib = true
-
rescue
-
@@__have_zlib = false
-
end
-
-
1
require 'rexml/document'
-
-
1
module SVG
-
1
module Graph
-
1
VERSION = '@ANT_VERSION@'
-
-
# === Base object for generating SVG Graphs
-
#
-
# == Synopsis
-
#
-
# This class is only used as a superclass of specialized charts. Do not
-
# attempt to use this class directly, unless creating a new chart type.
-
#
-
# For examples of how to subclass this class, see the existing specific
-
# subclasses, such as SVG::Graph::Pie.
-
#
-
# == Examples
-
#
-
# For examples of how to use this package, see either the test files, or
-
# the documentation for the specific class you want to use.
-
#
-
# * file:test/plot.rb
-
# * file:test/single.rb
-
# * file:test/test.rb
-
# * file:test/timeseries.rb
-
#
-
# == Description
-
#
-
# This package should be used as a base for creating SVG graphs.
-
#
-
# == Acknowledgements
-
#
-
# Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
-
# port is based on.
-
#
-
# Stephen Morgan for creating the TT template and SVG.
-
#
-
# == See
-
#
-
# * SVG::Graph::BarHorizontal
-
# * SVG::Graph::Bar
-
# * SVG::Graph::Line
-
# * SVG::Graph::Pie
-
# * SVG::Graph::Plot
-
# * SVG::Graph::TimeSeries
-
#
-
# == Author
-
#
-
# Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
-
#
-
# Copyright 2004 Sean E. Russell
-
# This software is available under the Ruby license[LICENSE.txt]
-
#
-
1
class Graph
-
1
include REXML
-
-
# Initialize the graph object with the graph settings. You won't
-
# instantiate this class directly; see the subclass for options.
-
# [width] 500
-
# [height] 300
-
# [show_x_guidelines] false
-
# [show_y_guidelines] true
-
# [show_data_values] true
-
# [min_scale_value] 0
-
# [show_x_labels] true
-
# [stagger_x_labels] false
-
# [rotate_x_labels] false
-
# [step_x_labels] 1
-
# [step_include_first_x_label] true
-
# [show_y_labels] true
-
# [rotate_y_labels] false
-
# [scale_integers] false
-
# [show_x_title] false
-
# [x_title] 'X Field names'
-
# [show_y_title] false
-
# [y_title_text_direction] :bt
-
# [y_title] 'Y Scale'
-
# [show_graph_title] false
-
# [graph_title] 'Graph Title'
-
# [show_graph_subtitle] false
-
# [graph_subtitle] 'Graph Sub Title'
-
# [key] true,
-
# [key_position] :right, # bottom or righ
-
# [font_size] 12
-
# [title_font_size] 16
-
# [subtitle_font_size] 14
-
# [x_label_font_size] 12
-
# [x_title_font_size] 14
-
# [y_label_font_size] 12
-
# [y_title_font_size] 14
-
# [key_font_size] 10
-
# [no_css] false
-
# [add_popups] false
-
1
def initialize( config )
-
@config = config
-
-
self.top_align = self.top_font = self.right_align = self.right_font = 0
-
-
init_with({
-
:width => 500,
-
:height => 300,
-
:show_x_guidelines => false,
-
:show_y_guidelines => true,
-
:show_data_values => true,
-
-
# :min_scale_value => 0,
-
-
:show_x_labels => true,
-
:stagger_x_labels => false,
-
:rotate_x_labels => false,
-
:step_x_labels => 1,
-
:step_include_first_x_label => true,
-
-
:show_y_labels => true,
-
:rotate_y_labels => false,
-
:stagger_y_labels => false,
-
:scale_integers => false,
-
-
:show_x_title => false,
-
:x_title => 'X Field names',
-
-
:show_y_title => false,
-
:y_title_text_direction => :bt,
-
:y_title => 'Y Scale',
-
-
:show_graph_title => false,
-
:graph_title => 'Graph Title',
-
:show_graph_subtitle => false,
-
:graph_subtitle => 'Graph Sub Title',
-
:key => true,
-
:key_position => :right, # bottom or right
-
-
:font_size =>12,
-
:title_font_size =>16,
-
:subtitle_font_size =>14,
-
:x_label_font_size =>12,
-
:x_title_font_size =>14,
-
:y_label_font_size =>12,
-
:y_title_font_size =>14,
-
:key_font_size =>10,
-
-
:no_css =>false,
-
:add_popups =>false,
-
})
-
-
set_defaults if respond_to? :set_defaults
-
-
init_with config
-
end
-
-
-
# This method allows you do add data to the graph object.
-
# It can be called several times to add more data sets in.
-
#
-
# data_sales_02 = [12, 45, 21];
-
#
-
# graph.add_data({
-
# :data => data_sales_02,
-
# :title => 'Sales 2002'
-
# })
-
1
def add_data conf
-
@data = [] unless defined? @data
-
-
if conf[:data] and conf[:data].kind_of? Array
-
@data << conf
-
else
-
raise "No data provided by #{conf.inspect}"
-
end
-
end
-
-
-
# This method removes all data from the object so that you can
-
# reuse it to create a new graph but with the same config options.
-
#
-
# graph.clear_data
-
1
def clear_data
-
@data = []
-
end
-
-
-
# This method processes the template with the data and
-
# config which has been set and returns the resulting SVG.
-
#
-
# This method will croak unless at least one data set has
-
# been added to the graph object.
-
#
-
# print graph.burn
-
1
def burn
-
raise "No data available" unless @data.size > 0
-
-
calculations if respond_to? :calculations
-
-
start_svg
-
calculate_graph_dimensions
-
@foreground = Element.new( "g" )
-
draw_graph
-
draw_titles
-
draw_legend
-
draw_data
-
@graph.add_element( @foreground )
-
style
-
-
data = ""
-
@doc.write( data, 0 )
-
-
if @config[:compress]
-
if @@__have_zlib
-
inp, out = IO.pipe
-
gz = Zlib::GzipWriter.new( out )
-
gz.write data
-
gz.close
-
data = inp.read
-
else
-
data << "<!-- Ruby Zlib not available for SVGZ -->";
-
end
-
end
-
-
return data
-
end
-
-
-
# Set the height of the graph box, this is the total height
-
# of the SVG box created - not the graph it self which auto
-
# scales to fix the space.
-
1
attr_accessor :height
-
# Set the width of the graph box, this is the total width
-
# of the SVG box created - not the graph it self which auto
-
# scales to fix the space.
-
1
attr_accessor :width
-
# Set the path to an external stylesheet, set to '' if
-
# you want to revert back to using the defaut internal version.
-
#
-
# To create an external stylesheet create a graph using the
-
# default internal version and copy the stylesheet section to
-
# an external file and edit from there.
-
1
attr_accessor :style_sheet
-
# (Bool) Show the value of each element of data on the graph
-
1
attr_accessor :show_data_values
-
# The point at which the Y axis starts, defaults to '0',
-
# if set to nil it will default to the minimum data value.
-
1
attr_accessor :min_scale_value
-
# Whether to show labels on the X axis or not, defaults
-
# to true, set to false if you want to turn them off.
-
1
attr_accessor :show_x_labels
-
# This puts the X labels at alternative levels so if they
-
# are long field names they will not overlap so easily.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :stagger_x_labels
-
# This puts the Y labels at alternative levels so if they
-
# are long field names they will not overlap so easily.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :stagger_y_labels
-
# This turns the X axis labels by 90 degrees.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :rotate_x_labels
-
# This turns the Y axis labels by 90 degrees.
-
# Default it false, to turn on set to true.
-
1
attr_accessor :rotate_y_labels
-
# How many "steps" to use between displayed X axis labels,
-
# a step of one means display every label, a step of two results
-
# in every other label being displayed (label <gap> label <gap> label),
-
# a step of three results in every third label being displayed
-
# (label <gap> <gap> label <gap> <gap> label) and so on.
-
1
attr_accessor :step_x_labels
-
# Whether to (when taking "steps" between X axis labels) step from
-
# the first label (i.e. always include the first label) or step from
-
# the X axis origin (i.e. start with a gap if step_x_labels is greater
-
# than one).
-
1
attr_accessor :step_include_first_x_label
-
# Whether to show labels on the Y axis or not, defaults
-
# to true, set to false if you want to turn them off.
-
1
attr_accessor :show_y_labels
-
# Ensures only whole numbers are used as the scale divisions.
-
# Default it false, to turn on set to true. This has no effect if
-
# scale divisions are less than 1.
-
1
attr_accessor :scale_integers
-
# This defines the gap between markers on the Y axis,
-
# default is a 10th of the max_value, e.g. you will have
-
# 10 markers on the Y axis. NOTE: do not set this too
-
# low - you are limited to 999 markers, after that the
-
# graph won't generate.
-
1
attr_accessor :scale_divisions
-
# Whether to show the title under the X axis labels,
-
# default is false, set to true to show.
-
1
attr_accessor :show_x_title
-
# What the title under X axis should be, e.g. 'Months'.
-
1
attr_accessor :x_title
-
# Whether to show the title under the Y axis labels,
-
# default is false, set to true to show.
-
1
attr_accessor :show_y_title
-
# Aligns writing mode for Y axis label.
-
# Defaults to :bt (Bottom to Top).
-
# Change to :tb (Top to Bottom) to reverse.
-
1
attr_accessor :y_title_text_direction
-
# What the title under Y axis should be, e.g. 'Sales in thousands'.
-
1
attr_accessor :y_title
-
# Whether to show a title on the graph, defaults
-
# to false, set to true to show.
-
1
attr_accessor :show_graph_title
-
# What the title on the graph should be.
-
1
attr_accessor :graph_title
-
# Whether to show a subtitle on the graph, defaults
-
# to false, set to true to show.
-
1
attr_accessor :show_graph_subtitle
-
# What the subtitle on the graph should be.
-
1
attr_accessor :graph_subtitle
-
# Whether to show a key, defaults to false, set to
-
# true if you want to show it.
-
1
attr_accessor :key
-
# Where the key should be positioned, defaults to
-
# :right, set to :bottom if you want to move it.
-
1
attr_accessor :key_position
-
# Set the font size (in points) of the data point labels
-
1
attr_accessor :font_size
-
# Set the font size of the X axis labels
-
1
attr_accessor :x_label_font_size
-
# Set the font size of the X axis title
-
1
attr_accessor :x_title_font_size
-
# Set the font size of the Y axis labels
-
1
attr_accessor :y_label_font_size
-
# Set the font size of the Y axis title
-
1
attr_accessor :y_title_font_size
-
# Set the title font size
-
1
attr_accessor :title_font_size
-
# Set the subtitle font size
-
1
attr_accessor :subtitle_font_size
-
# Set the key font size
-
1
attr_accessor :key_font_size
-
# Show guidelines for the X axis
-
1
attr_accessor :show_x_guidelines
-
# Show guidelines for the Y axis
-
1
attr_accessor :show_y_guidelines
-
# Do not use CSS if set to true. Many SVG viewers do not support CSS, but
-
# not using CSS can result in larger SVGs as well as making it impossible to
-
# change colors after the chart is generated. Defaults to false.
-
1
attr_accessor :no_css
-
# Add popups for the data points on some graphs
-
1
attr_accessor :add_popups
-
-
-
1
protected
-
-
1
def sort( *arrys )
-
sort_multiple( arrys )
-
end
-
-
# Overwrite configuration options with supplied options. Used
-
# by subclasses.
-
1
def init_with config
-
config.each { |key, value|
-
self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym
-
}
-
end
-
-
1
attr_accessor :top_align, :top_font, :right_align, :right_font
-
-
1
KEY_BOX_SIZE = 12
-
-
# Override this (and call super) to change the margin to the left
-
# of the plot area. Results in @border_left being set.
-
1
def calculate_left_margin
-
@border_left = 7
-
# Check for Y labels
-
max_y_label_height_px = rotate_y_labels ?
-
y_label_font_size :
-
get_y_labels.max{|a,b|
-
a.to_s.length<=>b.to_s.length
-
}.to_s.length * y_label_font_size * 0.6
-
@border_left += max_y_label_height_px if show_y_labels
-
@border_left += max_y_label_height_px + 10 if stagger_y_labels
-
@border_left += y_title_font_size + 5 if show_y_title
-
end
-
-
-
# Calculates the width of the widest Y label. This will be the
-
# character height if the Y labels are rotated
-
1
def max_y_label_width_px
-
return font_size if rotate_y_labels
-
end
-
-
-
# Override this (and call super) to change the margin to the right
-
# of the plot area. Results in @border_right being set.
-
1
def calculate_right_margin
-
@border_right = 7
-
if key and key_position == :right
-
val = keys.max { |a,b| a.length <=> b.length }
-
@border_right += val.length * key_font_size * 0.6
-
@border_right += KEY_BOX_SIZE
-
@border_right += 10 # Some padding around the box
-
end
-
end
-
-
-
# Override this (and call super) to change the margin to the top
-
# of the plot area. Results in @border_top being set.
-
1
def calculate_top_margin
-
@border_top = 5
-
@border_top += title_font_size if show_graph_title
-
@border_top += 5
-
@border_top += subtitle_font_size if show_graph_subtitle
-
end
-
-
-
# Adds pop-up point information to a graph.
-
1
def add_popup( x, y, label )
-
txt_width = label.length * font_size * 0.6 + 10
-
tx = (x+txt_width > width ? x-5 : x+5)
-
t = @foreground.add_element( "text", {
-
"x" => tx.to_s,
-
"y" => (y - font_size).to_s,
-
"visibility" => "hidden",
-
})
-
t.attributes["style"] = "fill: #000; "+
-
(x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
-
t.text = label.to_s
-
t.attributes["id"] = t.object_id.to_s
-
-
@foreground.add_element( "circle", {
-
"cx" => x.to_s,
-
"cy" => y.to_s,
-
"r" => "10",
-
"style" => "opacity: 0",
-
"onmouseover" =>
-
"document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
-
"onmouseout" =>
-
"document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
-
})
-
-
end
-
-
-
# Override this (and call super) to change the margin to the bottom
-
# of the plot area. Results in @border_bottom being set.
-
1
def calculate_bottom_margin
-
@border_bottom = 7
-
if key and key_position == :bottom
-
@border_bottom += @data.size * (font_size + 5)
-
@border_bottom += 10
-
end
-
if show_x_labels
-
max_x_label_height_px = (not rotate_x_labels) ?
-
x_label_font_size :
-
get_x_labels.max{|a,b|
-
a.to_s.length<=>b.to_s.length
-
}.to_s.length * x_label_font_size * 0.6
-
@border_bottom += max_x_label_height_px
-
@border_bottom += max_x_label_height_px + 10 if stagger_x_labels
-
end
-
@border_bottom += x_title_font_size + 5 if show_x_title
-
end
-
-
-
# Draws the background, axis, and labels.
-
1
def draw_graph
-
@graph = @root.add_element( "g", {
-
"transform" => "translate( #@border_left #@border_top )"
-
})
-
-
# Background
-
@graph.add_element( "rect", {
-
"x" => "0",
-
"y" => "0",
-
"width" => @graph_width.to_s,
-
"height" => @graph_height.to_s,
-
"class" => "graphBackground"
-
})
-
-
# Axis
-
@graph.add_element( "path", {
-
"d" => "M 0 0 v#@graph_height",
-
"class" => "axis",
-
"id" => "xAxis"
-
})
-
@graph.add_element( "path", {
-
"d" => "M 0 #@graph_height h#@graph_width",
-
"class" => "axis",
-
"id" => "yAxis"
-
})
-
-
draw_x_labels
-
draw_y_labels
-
end
-
-
-
# Where in the X area the label is drawn
-
# Centered in the field, should be width/2. Start, 0.
-
1
def x_label_offset( width )
-
0
-
end
-
-
1
def make_datapoint_text( x, y, value, style="" )
-
if show_data_values
-
@foreground.add_element( "text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "dataPointLabel",
-
"style" => "#{style} stroke: #fff; stroke-width: 2;"
-
}).text = value.to_s
-
text = @foreground.add_element( "text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "dataPointLabel"
-
})
-
text.text = value.to_s
-
text.attributes["style"] = style if style.length > 0
-
end
-
end
-
-
-
# Draws the X axis labels
-
1
def draw_x_labels
-
stagger = x_label_font_size + 5
-
if show_x_labels
-
label_width = field_width
-
-
count = 0
-
for label in get_x_labels
-
if step_include_first_x_label == true then
-
step = count % step_x_labels
-
else
-
step = (count + 1) % step_x_labels
-
end
-
-
if step == 0 then
-
text = @graph.add_element( "text" )
-
text.attributes["class"] = "xAxisLabels"
-
text.text = label.to_s
-
-
x = count * label_width + x_label_offset( label_width )
-
y = @graph_height + x_label_font_size + 3
-
t = 0 - (font_size / 2)
-
-
if stagger_x_labels and count % 2 == 1
-
y += stagger
-
@graph.add_element( "path", {
-
"d" => "M#{x} #@graph_height v#{stagger}",
-
"class" => "staggerGuideLine"
-
})
-
end
-
-
text.attributes["x"] = x.to_s
-
text.attributes["y"] = y.to_s
-
if rotate_x_labels
-
text.attributes["transform"] =
-
"rotate( 90 #{x} #{y-x_label_font_size} )"+
-
" translate( 0 -#{x_label_font_size/4} )"
-
text.attributes["style"] = "text-anchor: start"
-
else
-
text.attributes["style"] = "text-anchor: middle"
-
end
-
end
-
-
draw_x_guidelines( label_width, count ) if show_x_guidelines
-
count += 1
-
end
-
end
-
end
-
-
-
# Where in the Y area the label is drawn
-
# Centered in the field, should be width/2. Start, 0.
-
1
def y_label_offset( height )
-
0
-
end
-
-
-
1
def field_width
-
(@graph_width.to_f - font_size*2*right_font) /
-
(get_x_labels.length - right_align)
-
end
-
-
-
1
def field_height
-
(@graph_height.to_f - font_size*2*top_font) /
-
(get_y_labels.length - top_align)
-
end
-
-
-
# Draws the Y axis labels
-
1
def draw_y_labels
-
stagger = y_label_font_size + 5
-
if show_y_labels
-
label_height = field_height
-
-
count = 0
-
y_offset = @graph_height + y_label_offset( label_height )
-
y_offset += font_size/1.2 unless rotate_y_labels
-
for label in get_y_labels
-
y = y_offset - (label_height * count)
-
x = rotate_y_labels ? 0 : -3
-
-
if stagger_y_labels and count % 2 == 1
-
x -= stagger
-
@graph.add_element( "path", {
-
"d" => "M#{x} #{y} h#{stagger}",
-
"class" => "staggerGuideLine"
-
})
-
end
-
-
text = @graph.add_element( "text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "yAxisLabels"
-
})
-
text.text = label.to_s
-
if rotate_y_labels
-
text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
-
"rotate( 90 #{x} #{y} ) "
-
text.attributes["style"] = "text-anchor: middle"
-
else
-
text.attributes["y"] = (y - (y_label_font_size/2)).to_s
-
text.attributes["style"] = "text-anchor: end"
-
end
-
draw_y_guidelines( label_height, count ) if show_y_guidelines
-
count += 1
-
end
-
end
-
end
-
-
-
# Draws the X axis guidelines
-
1
def draw_x_guidelines( label_height, count )
-
if count != 0
-
@graph.add_element( "path", {
-
"d" => "M#{label_height*count} 0 v#@graph_height",
-
"class" => "guideLines"
-
})
-
end
-
end
-
-
-
# Draws the Y axis guidelines
-
1
def draw_y_guidelines( label_height, count )
-
if count != 0
-
@graph.add_element( "path", {
-
"d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
-
"class" => "guideLines"
-
})
-
end
-
end
-
-
-
# Draws the graph title and subtitle
-
1
def draw_titles
-
if show_graph_title
-
@root.add_element( "text", {
-
"x" => (width / 2).to_s,
-
"y" => (title_font_size).to_s,
-
"class" => "mainTitle"
-
}).text = graph_title.to_s
-
end
-
-
if show_graph_subtitle
-
y_subtitle = show_graph_title ?
-
title_font_size + 10 :
-
subtitle_font_size
-
@root.add_element("text", {
-
"x" => (width / 2).to_s,
-
"y" => (y_subtitle).to_s,
-
"class" => "subTitle"
-
}).text = graph_subtitle.to_s
-
end
-
-
if show_x_title
-
y = @graph_height + @border_top + x_title_font_size
-
if show_x_labels
-
y += x_label_font_size + 5 if stagger_x_labels
-
y += x_label_font_size + 5
-
end
-
x = width / 2
-
-
@root.add_element("text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "xAxisTitle",
-
}).text = x_title.to_s
-
end
-
-
if show_y_title
-
x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
-
y = height / 2
-
-
text = @root.add_element("text", {
-
"x" => x.to_s,
-
"y" => y.to_s,
-
"class" => "yAxisTitle",
-
})
-
text.text = y_title.to_s
-
if y_title_text_direction == :bt
-
text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
-
else
-
text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
-
end
-
end
-
end
-
-
1
def keys
-
return @data.collect{ |d| d[:title] }
-
end
-
-
# Draws the legend on the graph
-
1
def draw_legend
-
if key
-
group = @root.add_element( "g" )
-
-
key_count = 0
-
for key_name in keys
-
y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
-
group.add_element( "rect", {
-
"x" => 0.to_s,
-
"y" => y_offset.to_s,
-
"width" => KEY_BOX_SIZE.to_s,
-
"height" => KEY_BOX_SIZE.to_s,
-
"class" => "key#{key_count+1}"
-
})
-
group.add_element( "text", {
-
"x" => (KEY_BOX_SIZE + 5).to_s,
-
"y" => (y_offset + KEY_BOX_SIZE).to_s,
-
"class" => "keyText"
-
}).text = key_name.to_s
-
key_count += 1
-
end
-
-
case key_position
-
when :right
-
x_offset = @graph_width + @border_left + 10
-
y_offset = @border_top + 20
-
when :bottom
-
x_offset = @border_left + 20
-
y_offset = @border_top + @graph_height + 5
-
if show_x_labels
-
max_x_label_height_px = (not rotate_x_labels) ?
-
x_label_font_size :
-
get_x_labels.max{|a,b|
-
a.to_s.length<=>b.to_s.length
-
}.to_s.length * x_label_font_size * 0.6
-
x_label_font_size
-
y_offset += max_x_label_height_px
-
y_offset += max_x_label_height_px + 5 if stagger_x_labels
-
end
-
y_offset += x_title_font_size + 5 if show_x_title
-
end
-
group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
-
end
-
end
-
-
-
1
private
-
-
1
def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
-
if lo < hi
-
p = partition(arrys,lo,hi)
-
sort_multiple(arrys, lo, p-1)
-
sort_multiple(arrys, p+1, hi)
-
end
-
arrys
-
end
-
-
1
def partition( arrys, lo, hi )
-
p = arrys[0][lo]
-
l = lo
-
z = lo+1
-
while z <= hi
-
if arrys[0][z] < p
-
l += 1
-
arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
-
end
-
z += 1
-
end
-
arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
-
l
-
end
-
-
1
def style
-
if no_css
-
styles = parse_css
-
@root.elements.each("//*[@class]") { |el|
-
cl = el.attributes["class"]
-
style = styles[cl]
-
style += el.attributes["style"] if el.attributes["style"]
-
el.attributes["style"] = style
-
}
-
end
-
end
-
-
1
def parse_css
-
css = get_style
-
rv = {}
-
while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
-
names_orig = names = $1
-
css = $'
-
css =~ /([^}]+)\}/m
-
content = $1
-
css = $'
-
-
nms = []
-
while names =~ /^\s*,?\s*\.(\w+)/
-
nms << $1
-
names = $'
-
end
-
-
content = content.tr( "\n\t", " ")
-
for name in nms
-
current = rv[name]
-
current = current ? current+"; "+content : content
-
rv[name] = current.strip.squeeze(" ")
-
end
-
end
-
return rv
-
end
-
-
-
# Override and place code to add defs here
-
1
def add_defs defs
-
end
-
-
-
1
def start_svg
-
# Base document
-
@doc = Document.new
-
@doc << XMLDecl.new
-
@doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
-
%q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
-
if style_sheet && style_sheet != ''
-
@doc << Instruction.new( "xml-stylesheet",
-
%Q{href="#{style_sheet}" type="text/css"} )
-
end
-
@root = @doc.add_element( "svg", {
-
"width" => width.to_s,
-
"height" => height.to_s,
-
"viewBox" => "0 0 #{width} #{height}",
-
"xmlns" => "http://www.w3.org/2000/svg",
-
"xmlns:xlink" => "http://www.w3.org/1999/xlink",
-
"xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
-
"a3:scriptImplementation" => "Adobe"
-
})
-
@root << Comment.new( " "+"\\"*66 )
-
@root << Comment.new( " Created with SVG::Graph " )
-
@root << Comment.new( " SVG::Graph by Sean E. Russell " )
-
@root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
-
" Leo Lapworth & Stephan Morgan " )
-
@root << Comment.new( " "+"/"*66 )
-
-
defs = @root.add_element( "defs" )
-
add_defs defs
-
if not(style_sheet && style_sheet != '') and !no_css
-
@root << Comment.new(" include default stylesheet if none specified ")
-
style = defs.add_element( "style", {"type"=>"text/css"} )
-
style << CData.new( get_style )
-
end
-
-
@root << Comment.new( "SVG Background" )
-
@root.add_element( "rect", {
-
"width" => width.to_s,
-
"height" => height.to_s,
-
"x" => "0",
-
"y" => "0",
-
"class" => "svgBackground"
-
})
-
end
-
-
-
1
def calculate_graph_dimensions
-
calculate_left_margin
-
calculate_right_margin
-
calculate_bottom_margin
-
calculate_top_margin
-
@graph_width = width - @border_left - @border_right
-
@graph_height = height - @border_top - @border_bottom
-
end
-
-
1
def get_style
-
return <<EOL
-
/* Copy from here for external style sheet */
-
.svgBackground{
-
fill:#ffffff;
-
}
-
.graphBackground{
-
fill:#f0f0f0;
-
}
-
-
/* graphs titles */
-
.mainTitle{
-
text-anchor: middle;
-
fill: #000000;
-
font-size: #{title_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
.subTitle{
-
text-anchor: middle;
-
fill: #999999;
-
font-size: #{subtitle_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.axis{
-
stroke: #000000;
-
stroke-width: 1px;
-
}
-
-
.guideLines{
-
stroke: #666666;
-
stroke-width: 1px;
-
stroke-dasharray: 5 5;
-
}
-
-
.xAxisLabels{
-
text-anchor: middle;
-
fill: #000000;
-
font-size: #{x_label_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.yAxisLabels{
-
text-anchor: end;
-
fill: #000000;
-
font-size: #{y_label_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.xAxisTitle{
-
text-anchor: middle;
-
fill: #ff0000;
-
font-size: #{x_title_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.yAxisTitle{
-
fill: #ff0000;
-
text-anchor: middle;
-
font-size: #{y_title_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.dataPointLabel{
-
fill: #000000;
-
text-anchor:middle;
-
font-size: 10px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
-
.staggerGuideLine{
-
fill: none;
-
stroke: #000000;
-
stroke-width: 0.5px;
-
}
-
-
#{get_css}
-
-
.keyText{
-
fill: #000000;
-
text-anchor:start;
-
font-size: #{key_font_size}px;
-
font-family: "Arial", sans-serif;
-
font-weight: normal;
-
}
-
/* End copy for external style sheet */
-
EOL
-
end
-
-
end
-
end
-
end
-
1
module RedmineDiff
-
1
class Diff
-
-
1
VERSION = 0.3
-
-
1
def Diff.lcs(a, b)
-
astart = 0
-
bstart = 0
-
afinish = a.length-1
-
bfinish = b.length-1
-
mvector = []
-
-
# First we prune off any common elements at the beginning
-
while (astart <= afinish && bstart <= afinish && a[astart] == b[bstart])
-
mvector[astart] = bstart
-
astart += 1
-
bstart += 1
-
end
-
-
# now the end
-
while (astart <= afinish && bstart <= bfinish && a[afinish] == b[bfinish])
-
mvector[afinish] = bfinish
-
afinish -= 1
-
bfinish -= 1
-
end
-
-
bmatches = b.reverse_hash(bstart..bfinish)
-
thresh = []
-
links = []
-
-
(astart..afinish).each { |aindex|
-
aelem = a[aindex]
-
next unless bmatches.has_key? aelem
-
k = nil
-
bmatches[aelem].reverse.each { |bindex|
-
if k && (thresh[k] > bindex) && (thresh[k-1] < bindex)
-
thresh[k] = bindex
-
else
-
k = thresh.replacenextlarger(bindex, k)
-
end
-
links[k] = [ (k==0) ? nil : links[k-1], aindex, bindex ] if k
-
}
-
}
-
-
if !thresh.empty?
-
link = links[thresh.length-1]
-
while link
-
mvector[link[1]] = link[2]
-
link = link[0]
-
end
-
end
-
-
return mvector
-
end
-
-
1
def makediff(a, b)
-
mvector = Diff.lcs(a, b)
-
ai = bi = 0
-
while ai < mvector.length
-
bline = mvector[ai]
-
if bline
-
while bi < bline
-
discardb(bi, b[bi])
-
bi += 1
-
end
-
match(ai, bi)
-
bi += 1
-
else
-
discarda(ai, a[ai])
-
end
-
ai += 1
-
end
-
while ai < a.length
-
discarda(ai, a[ai])
-
ai += 1
-
end
-
while bi < b.length
-
discardb(bi, b[bi])
-
bi += 1
-
end
-
match(ai, bi)
-
1
-
end
-
-
1
def compactdiffs
-
diffs = []
-
@diffs.each { |df|
-
i = 0
-
curdiff = []
-
while i < df.length
-
whot = df[i][0]
-
s = @isstring ? df[i][2].chr : [df[i][2]]
-
p = df[i][1]
-
last = df[i][1]
-
i += 1
-
while df[i] && df[i][0] == whot && df[i][1] == last+1
-
s << df[i][2]
-
last = df[i][1]
-
i += 1
-
end
-
curdiff.push [whot, p, s]
-
end
-
diffs.push curdiff
-
}
-
return diffs
-
end
-
-
1
attr_reader :diffs, :difftype
-
-
1
def initialize(diffs_or_a, b = nil, isstring = nil)
-
if b.nil?
-
@diffs = diffs_or_a
-
@isstring = isstring
-
else
-
@diffs = []
-
@curdiffs = []
-
makediff(diffs_or_a, b)
-
@difftype = diffs_or_a.class
-
end
-
end
-
-
1
def match(ai, bi)
-
@diffs.push @curdiffs unless @curdiffs.empty?
-
@curdiffs = []
-
end
-
-
1
def discarda(i, elem)
-
@curdiffs.push ['-', i, elem]
-
end
-
-
1
def discardb(i, elem)
-
@curdiffs.push ['+', i, elem]
-
end
-
-
1
def compact
-
return Diff.new(compactdiffs)
-
end
-
-
1
def compact!
-
@diffs = compactdiffs
-
end
-
-
1
def inspect
-
@diffs.inspect
-
end
-
-
end
-
end
-
-
1
module Diffable
-
1
def diff(b)
-
RedmineDiff::Diff.new(self, b)
-
end
-
-
# Create a hash that maps elements of the array to arrays of indices
-
# where the elements are found.
-
-
1
def reverse_hash(range = (0...self.length))
-
revmap = {}
-
range.each { |i|
-
elem = self[i]
-
if revmap.has_key? elem
-
revmap[elem].push i
-
else
-
revmap[elem] = [i]
-
end
-
}
-
return revmap
-
end
-
-
1
def replacenextlarger(value, high = nil)
-
high ||= self.length
-
if self.empty? || value > self[-1]
-
push value
-
return high
-
end
-
# binary search for replacement point
-
low = 0
-
while low < high
-
index = (high+low)/2
-
found = self[index]
-
return nil if value == found
-
if value > found
-
low = index + 1
-
else
-
high = index
-
end
-
end
-
-
self[low] = value
-
# $stderr << "replace #{value} : 0/#{low}/#{init_high} (#{steps} steps) (#{init_high-low} off )\n"
-
# $stderr.puts self.inspect
-
#gets
-
#p length - low
-
return low
-
end
-
-
1
def patch(diff)
-
newary = nil
-
if diff.difftype == String
-
newary = diff.difftype.new('')
-
else
-
newary = diff.difftype.new
-
end
-
ai = 0
-
bi = 0
-
diff.diffs.each { |d|
-
d.each { |mod|
-
case mod[0]
-
when '-'
-
while ai < mod[1]
-
newary << self[ai]
-
ai += 1
-
bi += 1
-
end
-
ai += 1
-
when '+'
-
while bi < mod[1]
-
newary << self[ai]
-
ai += 1
-
bi += 1
-
end
-
newary << mod[2]
-
bi += 1
-
else
-
raise "Unknown diff action"
-
end
-
}
-
}
-
while ai < self.length
-
newary << self[ai]
-
ai += 1
-
bi += 1
-
end
-
return newary
-
end
-
end
-
-
1
class Array
-
1
include Diffable
-
end
-
-
1
class String
-
1
include Diffable
-
end
-
-
=begin
-
= Diff
-
(({diff.rb})) - computes the differences between two arrays or
-
strings. Copyright (C) 2001 Lars Christensen
-
-
== Synopsis
-
-
diff = Diff.new(a, b)
-
b = a.patch(diff)
-
-
== Class Diff
-
=== Class Methods
-
--- Diff.new(a, b)
-
--- a.diff(b)
-
Creates a Diff object which represent the differences between
-
((|a|)) and ((|b|)). ((|a|)) and ((|b|)) can be either be arrays
-
of any objects, strings, or object of any class that include
-
module ((|Diffable|))
-
-
== Module Diffable
-
The module ((|Diffable|)) is intended to be included in any class for
-
which differences are to be computed. Diffable is included into String
-
and Array when (({diff.rb})) is (({require}))'d.
-
-
Classes including Diffable should implement (({[]})) to get element at
-
integer indices, (({<<})) to append elements to the object and
-
(({ClassName#new})) should accept 0 arguments to create a new empty
-
object.
-
-
=== Instance Methods
-
--- Diffable#patch(diff)
-
Applies the differences from ((|diff|)) to the object ((|obj|))
-
and return the result. ((|obj|)) is not changed. ((|obj|)) and
-
can be either an array or a string, but must match the object
-
from which the ((|diff|)) was created.
-
=end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module ActivityProvider
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_activity_provider(options = {})
-
10
unless self.included_modules.include?(Redmine::Acts::ActivityProvider::InstanceMethods)
-
9
cattr_accessor :activity_provider_options
-
9
send :include, Redmine::Acts::ActivityProvider::InstanceMethods
-
end
-
-
10
options.assert_valid_keys(:type, :permission, :timestamp, :author_key, :find_options)
-
10
self.activity_provider_options ||= {}
-
-
# One model can provide different event types
-
# We store these options in activity_provider_options hash
-
10
event_type = options.delete(:type) || self.name.underscore.pluralize
-
-
10
options[:timestamp] ||= "#{table_name}.created_on"
-
10
options[:find_options] ||= {}
-
10
options[:author_key] = "#{table_name}.#{options[:author_key]}" if options[:author_key].is_a?(Symbol)
-
10
self.activity_provider_options[event_type] = options
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
9
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Returns events of type event_type visible by user that occured between from and to
-
1
def find_events(event_type, user, from, to, options)
-
provider_options = activity_provider_options[event_type]
-
raise "#{self.name} can not provide #{event_type} events." if provider_options.nil?
-
-
scope = self
-
-
if from && to
-
scope = scope.scoped(:conditions => ["#{provider_options[:timestamp]} BETWEEN ? AND ?", from, to])
-
end
-
-
if options[:author]
-
return [] if provider_options[:author_key].nil?
-
scope = scope.scoped(:conditions => ["#{provider_options[:author_key]} = ?", options[:author].id])
-
end
-
-
if options[:limit]
-
# id and creation time should be in same order in most cases
-
scope = scope.scoped(:order => "#{table_name}.id DESC", :limit => options[:limit])
-
end
-
-
if provider_options.has_key?(:permission)
-
scope = scope.scoped(:conditions => Project.allowed_to_condition(user, provider_options[:permission] || :view_project, options))
-
elsif respond_to?(:visible)
-
scope = scope.visible(user, options)
-
else
-
ActiveSupport::Deprecation.warn "acts_as_activity_provider with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
-
scope = scope.scoped(:conditions => Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym, options))
-
end
-
-
scope.all(provider_options[:find_options].dup)
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Attachable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_attachable(options = {})
-
7
cattr_accessor :attachable_options
-
7
self.attachable_options = {}
-
7
attachable_options[:view_permission] = options.delete(:view_permission) || "view_#{self.name.pluralize.underscore}".to_sym
-
7
attachable_options[:delete_permission] = options.delete(:delete_permission) || "edit_#{self.name.pluralize.underscore}".to_sym
-
-
7
has_many :attachments, options.merge(:as => :container,
-
:order => "#{Attachment.table_name}.created_on",
-
:dependent => :destroy)
-
7
send :include, Redmine::Acts::Attachable::InstanceMethods
-
7
before_save :attach_saved_attachments
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
7
base.extend ClassMethods
-
end
-
-
1
def attachments_visible?(user=User.current)
-
(respond_to?(:visible?) ? visible?(user) : true) &&
-
user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
-
end
-
-
1
def attachments_deletable?(user=User.current)
-
(respond_to?(:visible?) ? visible?(user) : true) &&
-
user.allowed_to?(self.class.attachable_options[:delete_permission], self.project)
-
end
-
-
1
def saved_attachments
-
2213
@saved_attachments ||= []
-
end
-
-
1
def unsaved_attachments
-
4
@unsaved_attachments ||= []
-
end
-
-
1
def save_attachments(attachments, author=User.current)
-
2
if attachments.is_a?(Hash)
-
2
attachments = attachments.values
-
end
-
2
if attachments.is_a?(Array)
-
2
attachments.each do |attachment|
-
2
a = nil
-
2
if file = attachment['file']
-
next unless file.size > 0
-
a = Attachment.create(:file => file, :author => author)
-
elsif token = attachment['token']
-
a = Attachment.find_by_token(token)
-
next unless a
-
a.filename = attachment['filename'] unless attachment['filename'].blank?
-
a.content_type = attachment['content_type']
-
end
-
2
next unless a
-
a.description = attachment['description'].to_s.strip
-
if a.new_record?
-
unsaved_attachments << a
-
else
-
saved_attachments << a
-
end
-
end
-
end
-
2
{:files => saved_attachments, :unsaved => unsaved_attachments}
-
end
-
-
1
def attach_saved_attachments
-
2203
saved_attachments.each do |attachment|
-
self.attachments << attachment
-
end
-
end
-
-
1
module ClassMethods
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Customizable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_customizable(options = {})
-
7
return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
-
7
cattr_accessor :customizable_options
-
7
self.customizable_options = options
-
7
has_many :custom_values, :as => :customized,
-
:include => :custom_field,
-
:order => "#{CustomField.table_name}.position",
-
:dependent => :delete_all,
-
:validate => false
-
7
send :include, Redmine::Acts::Customizable::InstanceMethods
-
7
validate :validate_custom_field_values
-
7
after_save :save_custom_field_values
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
7
base.extend ClassMethods
-
end
-
-
1
def available_custom_fields
-
480
CustomField.find(:all, :conditions => "type = '#{self.class.name}CustomField'",
-
:order => 'position')
-
end
-
-
# Sets the values of the object's custom fields
-
# values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
-
1
def custom_fields=(values)
-
values_to_hash = values.inject({}) do |hash, v|
-
v = v.stringify_keys
-
if v['id'] && v.has_key?('value')
-
hash[v['id']] = v['value']
-
end
-
hash
-
end
-
self.custom_field_values = values_to_hash
-
end
-
-
# Sets the values of the object's custom fields
-
# values is a hash like {'1' => 'foo', 2 => 'bar'}
-
1
def custom_field_values=(values)
-
8
values = values.stringify_keys
-
-
8
custom_field_values.each do |custom_field_value|
-
2
key = custom_field_value.custom_field_id.to_s
-
2
if values.has_key?(key)
-
2
value = values[key]
-
2
if value.is_a?(Array)
-
value = value.reject(&:blank?).uniq
-
if value.empty?
-
value << ''
-
end
-
end
-
2
custom_field_value.value = value
-
end
-
end
-
8
@custom_field_values_changed = true
-
end
-
-
1
def custom_field_values
-
@custom_field_values ||= available_custom_fields.collect do |field|
-
603
x = CustomFieldValue.new
-
603
x.custom_field = field
-
603
x.customized = self
-
603
if field.multiple?
-
values = custom_values.select { |v| v.custom_field == field }
-
if values.empty?
-
values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
-
end
-
x.value = values.map(&:value)
-
else
-
1086
cv = custom_values.detect { |v| v.custom_field == field }
-
603
cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
-
603
x.value = cv.value
-
end
-
603
x
-
6528
end
-
end
-
-
1
def visible_custom_field_values
-
67
custom_field_values.select(&:visible?)
-
end
-
-
1
def custom_field_values_changed?
-
206
@custom_field_values_changed == true
-
end
-
-
1
def custom_value_for(c)
-
field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
-
custom_values.detect {|v| v.custom_field_id == field_id }
-
end
-
-
1
def custom_field_value(c)
-
field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
-
custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
-
end
-
-
1
def validate_custom_field_values
-
1659
if new_record? || custom_field_values_changed?
-
1453
custom_field_values.each(&:validate_value)
-
end
-
end
-
-
1
def save_custom_field_values
-
2325
target_custom_values = []
-
2325
custom_field_values.each do |custom_field_value|
-
533
if custom_field_value.value.is_a?(Array)
-
custom_field_value.value.each do |v|
-
target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
-
target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
-
target_custom_values << target
-
end
-
else
-
1189
target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
-
533
target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
-
533
target.value = custom_field_value.value
-
533
target_custom_values << target
-
end
-
end
-
2325
self.custom_values = target_custom_values
-
2325
custom_values.each(&:save)
-
2325
@custom_field_values_changed = false
-
2325
true
-
end
-
-
1
def reset_custom_values!
-
@custom_field_values = nil
-
@custom_field_values_changed = true
-
end
-
-
1
module ClassMethods
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Event
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_event(options = {})
-
11
return if self.included_modules.include?(Redmine::Acts::Event::InstanceMethods)
-
11
default_options = { :datetime => :created_on,
-
:title => :title,
-
:description => :description,
-
:author => :author,
-
:url => {:controller => 'welcome'},
-
:type => self.name.underscore.dasherize }
-
-
11
cattr_accessor :event_options
-
11
self.event_options = default_options.merge(options)
-
11
send :include, Redmine::Acts::Event::InstanceMethods
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
11
base.extend ClassMethods
-
end
-
-
1
%w(datetime title description author type).each do |attr|
-
5
src = <<-END_SRC
-
def event_#{attr}
-
option = event_options[:#{attr}]
-
if option.is_a?(Proc)
-
option.call(self)
-
elsif option.is_a?(Symbol)
-
send(option)
-
else
-
option
-
end
-
end
-
END_SRC
-
5
class_eval src, __FILE__, __LINE__
-
end
-
-
1
def event_date
-
event_datetime.to_date
-
end
-
-
1
def event_url(options = {})
-
option = event_options[:url]
-
if option.is_a?(Proc)
-
option.call(self).merge(options)
-
elsif option.is_a?(Hash)
-
option.merge(options)
-
elsif option.is_a?(Symbol)
-
send(option).merge(options)
-
else
-
option
-
end
-
end
-
-
# Returns the mail adresses of users that should be notified
-
1
def recipients
-
notified = project.notified_users
-
notified.reject! {|user| !visible?(user)}
-
notified.collect(&:mail)
-
end
-
-
1
module ClassMethods
-
end
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Acts #:nodoc:
-
1
module List #:nodoc:
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
-
# The class that has this specified needs to have a +position+ column defined as an integer on
-
# the mapped database table.
-
#
-
# Todo list example:
-
#
-
# class TodoList < ActiveRecord::Base
-
# has_many :todo_items, :order => "position"
-
# end
-
#
-
# class TodoItem < ActiveRecord::Base
-
# belongs_to :todo_list
-
# acts_as_list :scope => :todo_list
-
# end
-
#
-
# todo_list.first.move_to_bottom
-
# todo_list.last.move_higher
-
1
module ClassMethods
-
# Configuration options are:
-
#
-
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
-
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
-
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
-
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
-
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
-
1
def acts_as_list(options = {})
-
6
configuration = { :column => "position", :scope => "1 = 1" }
-
6
configuration.update(options) if options.is_a?(Hash)
-
-
6
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
-
-
6
if configuration[:scope].is_a?(Symbol)
-
1
scope_condition_method = %(
-
def scope_condition
-
if #{configuration[:scope].to_s}.nil?
-
"#{configuration[:scope].to_s} IS NULL"
-
else
-
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
-
end
-
end
-
)
-
else
-
5
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
-
end
-
-
class_eval <<-EOV
-
include ActiveRecord::Acts::List::InstanceMethods
-
-
def acts_as_list_class
-
::#{self.name}
-
end
-
-
def position_column
-
'#{configuration[:column]}'
-
end
-
-
#{scope_condition_method}
-
-
before_destroy :remove_from_list
-
before_create :add_to_list_bottom
-
6
EOV
-
end
-
end
-
-
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
-
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
-
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
-
# the first in the list of all chapters.
-
1
module InstanceMethods
-
# Insert the item at the given position (defaults to the top position of 1).
-
1
def insert_at(position = 1)
-
insert_at_position(position)
-
end
-
-
# Swap positions with the next lower item, if one exists.
-
1
def move_lower
-
return unless lower_item
-
-
acts_as_list_class.transaction do
-
lower_item.decrement_position
-
increment_position
-
end
-
end
-
-
# Swap positions with the next higher item, if one exists.
-
1
def move_higher
-
return unless higher_item
-
-
acts_as_list_class.transaction do
-
higher_item.increment_position
-
decrement_position
-
end
-
end
-
-
# Move to the bottom of the list. If the item is already in the list, the items below it have their
-
# position adjusted accordingly.
-
1
def move_to_bottom
-
return unless in_list?
-
acts_as_list_class.transaction do
-
decrement_positions_on_lower_items
-
assume_bottom_position
-
end
-
end
-
-
# Move to the top of the list. If the item is already in the list, the items above it have their
-
# position adjusted accordingly.
-
1
def move_to_top
-
return unless in_list?
-
acts_as_list_class.transaction do
-
increment_positions_on_higher_items
-
assume_top_position
-
end
-
end
-
-
# Move to the given position
-
1
def move_to=(pos)
-
case pos.to_s
-
when 'highest'
-
move_to_top
-
when 'higher'
-
move_higher
-
when 'lower'
-
move_lower
-
when 'lowest'
-
move_to_bottom
-
end
-
end
-
-
# Removes the item from the list.
-
1
def remove_from_list
-
if in_list?
-
decrement_positions_on_lower_items
-
update_attribute position_column, nil
-
end
-
end
-
-
# Increase the position of this item without adjusting the rest of the list.
-
1
def increment_position
-
return unless in_list?
-
update_attribute position_column, self.send(position_column).to_i + 1
-
end
-
-
# Decrease the position of this item without adjusting the rest of the list.
-
1
def decrement_position
-
return unless in_list?
-
update_attribute position_column, self.send(position_column).to_i - 1
-
end
-
-
# Return +true+ if this object is the first in the list.
-
1
def first?
-
return false unless in_list?
-
self.send(position_column) == 1
-
end
-
-
# Return +true+ if this object is the last in the list.
-
1
def last?
-
return false unless in_list?
-
self.send(position_column) == bottom_position_in_list
-
end
-
-
# Return the next higher item in the list.
-
1
def higher_item
-
return nil unless in_list?
-
acts_as_list_class.find(:first, :conditions =>
-
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
-
)
-
end
-
-
# Return the next lower item in the list.
-
1
def lower_item
-
return nil unless in_list?
-
acts_as_list_class.find(:first, :conditions =>
-
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
-
)
-
end
-
-
# Test if this record is in a list
-
1
def in_list?
-
!send(position_column).nil?
-
end
-
-
1
private
-
1
def add_to_list_top
-
increment_positions_on_all_items
-
end
-
-
1
def add_to_list_bottom
-
272
self[position_column] = bottom_position_in_list.to_i + 1
-
end
-
-
# Overwrite this method to define the scope of the list changes
-
1
def scope_condition() "1" end
-
-
# Returns the bottom position number in the list.
-
# bottom_position_in_list # => 2
-
1
def bottom_position_in_list(except = nil)
-
272
item = bottom_item(except)
-
272
item ? item.send(position_column) : 0
-
end
-
-
# Returns the bottom item
-
1
def bottom_item(except = nil)
-
272
conditions = scope_condition
-
272
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
-
272
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
-
end
-
-
# Forces item to assume the bottom position in the list.
-
1
def assume_bottom_position
-
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
-
end
-
-
# Forces item to assume the top position in the list.
-
1
def assume_top_position
-
update_attribute(position_column, 1)
-
end
-
-
# This has the effect of moving all the higher items up one.
-
1
def decrement_positions_on_higher_items(position)
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
-
)
-
end
-
-
# This has the effect of moving all the lower items up one.
-
1
def decrement_positions_on_lower_items
-
return unless in_list?
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
-
)
-
end
-
-
# This has the effect of moving all the higher items down one.
-
1
def increment_positions_on_higher_items
-
return unless in_list?
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
-
)
-
end
-
-
# This has the effect of moving all the lower items down one.
-
1
def increment_positions_on_lower_items(position)
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
-
)
-
end
-
-
# Increments position (<tt>position_column</tt>) of all items in the list.
-
1
def increment_positions_on_all_items
-
acts_as_list_class.update_all(
-
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
-
)
-
end
-
-
1
def insert_at_position(position)
-
remove_from_list
-
increment_positions_on_lower_items(position)
-
self.update_attribute(position_column, position)
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Acts
-
1
module Searchable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Options:
-
# * :columns - a column or an array of columns to search
-
# * :project_key - project foreign key (default to project_id)
-
# * :date_column - name of the datetime column (default to created_on)
-
# * :sort_order - name of the column used to sort results (default to :date_column or created_on)
-
# * :permission - permission required to search the model (default to :view_"objects")
-
1
def acts_as_searchable(options = {})
-
7
return if self.included_modules.include?(Redmine::Acts::Searchable::InstanceMethods)
-
-
7
cattr_accessor :searchable_options
-
7
self.searchable_options = options
-
-
7
if searchable_options[:columns].nil?
-
raise 'No searchable column defined.'
-
elsif !searchable_options[:columns].is_a?(Array)
-
1
searchable_options[:columns] = [] << searchable_options[:columns]
-
end
-
-
7
searchable_options[:project_key] ||= "#{table_name}.project_id"
-
7
searchable_options[:date_column] ||= "#{table_name}.created_on"
-
7
searchable_options[:order_column] ||= searchable_options[:date_column]
-
-
# Should we search custom fields on this model ?
-
7
searchable_options[:search_custom_fields] = !reflect_on_association(:custom_values).nil?
-
-
7
send :include, Redmine::Acts::Searchable::InstanceMethods
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
7
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Searches the model for the given tokens
-
# projects argument can be either nil (will search all projects), a project or an array of projects
-
# Returns the results and the results count
-
1
def search(tokens, projects=nil, options={})
-
if projects.is_a?(Array) && projects.empty?
-
# no results
-
return [[], 0]
-
end
-
-
# TODO: make user an argument
-
user = User.current
-
tokens = [] << tokens unless tokens.is_a?(Array)
-
projects = [] << projects unless projects.nil? || projects.is_a?(Array)
-
-
find_options = {:include => searchable_options[:include]}
-
find_options[:order] = "#{searchable_options[:order_column]} " + (options[:before] ? 'DESC' : 'ASC')
-
-
limit_options = {}
-
limit_options[:limit] = options[:limit] if options[:limit]
-
if options[:offset]
-
limit_options[:conditions] = "(#{searchable_options[:date_column]} " + (options[:before] ? '<' : '>') + "'#{connection.quoted_date(options[:offset])}')"
-
end
-
-
columns = searchable_options[:columns]
-
columns = columns[0..0] if options[:titles_only]
-
-
token_clauses = columns.collect {|column| "(LOWER(#{column}) LIKE ?)"}
-
-
if !options[:titles_only] && searchable_options[:search_custom_fields]
-
searchable_custom_field_ids = CustomField.find(:all,
-
:select => 'id',
-
:conditions => { :type => "#{self.name}CustomField",
-
:searchable => true }).collect(&:id)
-
if searchable_custom_field_ids.any?
-
custom_field_sql = "#{table_name}.id IN (SELECT customized_id FROM #{CustomValue.table_name}" +
-
" WHERE customized_type='#{self.name}' AND customized_id=#{table_name}.id AND LOWER(value) LIKE ?" +
-
" AND #{CustomValue.table_name}.custom_field_id IN (#{searchable_custom_field_ids.join(',')}))"
-
token_clauses << custom_field_sql
-
end
-
end
-
-
sql = (['(' + token_clauses.join(' OR ') + ')'] * tokens.size).join(options[:all_words] ? ' AND ' : ' OR ')
-
-
find_options[:conditions] = [sql, * (tokens.collect {|w| "%#{w.downcase}%"} * token_clauses.size).sort]
-
-
scope = self
-
project_conditions = []
-
if searchable_options.has_key?(:permission)
-
project_conditions << Project.allowed_to_condition(user, searchable_options[:permission] || :view_project)
-
elsif respond_to?(:visible)
-
scope = scope.visible(user)
-
else
-
ActiveSupport::Deprecation.warn "acts_as_searchable with implicit :permission option is deprecated. Add a visible scope to the #{self.name} model or use explicit :permission option."
-
project_conditions << Project.allowed_to_condition(user, "view_#{self.name.underscore.pluralize}".to_sym)
-
end
-
# TODO: use visible scope options instead
-
project_conditions << "#{searchable_options[:project_key]} IN (#{projects.collect(&:id).join(',')})" unless projects.nil?
-
project_conditions = project_conditions.empty? ? nil : project_conditions.join(' AND ')
-
-
results = []
-
results_count = 0
-
-
scope = scope.scoped({:conditions => project_conditions}).scoped(find_options)
-
results_count = scope.count(:all)
-
results = scope.find(:all, limit_options)
-
-
[results, results_count]
-
end
-
end
-
end
-
end
-
end
-
end
-
1
module ActiveRecord
-
1
module Acts
-
1
module Tree
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
# Specify this +acts_as+ extension if you want to model a tree structure by providing a parent association and a children
-
# association. This requires that you have a foreign key column, which by default is called +parent_id+.
-
#
-
# class Category < ActiveRecord::Base
-
# acts_as_tree :order => "name"
-
# end
-
#
-
# Example:
-
# root
-
# \_ child1
-
# \_ subchild1
-
# \_ subchild2
-
#
-
# root = Category.create("name" => "root")
-
# child1 = root.children.create("name" => "child1")
-
# subchild1 = child1.children.create("name" => "subchild1")
-
#
-
# root.parent # => nil
-
# child1.parent # => root
-
# root.children # => [child1]
-
# root.children.first.children.first # => subchild1
-
#
-
# In addition to the parent and children associations, the following instance methods are added to the class
-
# after calling <tt>acts_as_tree</tt>:
-
# * <tt>siblings</tt> - Returns all the children of the parent, excluding the current node (<tt>[subchild2]</tt> when called on <tt>subchild1</tt>)
-
# * <tt>self_and_siblings</tt> - Returns all the children of the parent, including the current node (<tt>[subchild1, subchild2]</tt> when called on <tt>subchild1</tt>)
-
# * <tt>ancestors</tt> - Returns all the ancestors of the current node (<tt>[child1, root]</tt> when called on <tt>subchild2</tt>)
-
# * <tt>root</tt> - Returns the root of the current node (<tt>root</tt> when called on <tt>subchild2</tt>)
-
1
module ClassMethods
-
# Configuration options are:
-
#
-
# * <tt>foreign_key</tt> - specifies the column name to use for tracking of the tree (default: +parent_id+)
-
# * <tt>order</tt> - makes it possible to sort the children according to this SQL snippet.
-
# * <tt>counter_cache</tt> - keeps a count in a +children_count+ column if set to +true+ (default: +false+).
-
1
def acts_as_tree(options = {})
-
3
configuration = { :foreign_key => "parent_id", :dependent => :destroy, :order => nil, :counter_cache => nil }
-
3
configuration.update(options) if options.is_a?(Hash)
-
-
3
belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
-
3
has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => configuration[:dependent]
-
-
class_eval <<-EOV
-
include ActiveRecord::Acts::Tree::InstanceMethods
-
-
def self.roots
-
find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
-
end
-
-
def self.root
-
find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
-
end
-
3
EOV
-
end
-
end
-
-
1
module InstanceMethods
-
# Returns list of ancestors, starting from parent until root.
-
#
-
# subchild1.ancestors # => [child1, root]
-
1
def ancestors
-
node, nodes = self, []
-
nodes << node = node.parent while node.parent
-
nodes
-
end
-
-
# Returns list of descendants.
-
#
-
# root.descendants # => [child1, subchild1, subchild2]
-
1
def descendants
-
children + children.collect(&:children).flatten
-
end
-
-
# Returns list of descendants and a reference to the current node.
-
#
-
# root.self_and_descendants # => [root, child1, subchild1, subchild2]
-
1
def self_and_descendants
-
[self] + descendants
-
end
-
-
# Returns the root node of the tree.
-
1
def root
-
node = self
-
node = node.parent while node.parent
-
node
-
end
-
-
# Returns all siblings of the current node.
-
#
-
# subchild1.siblings # => [subchild2]
-
1
def siblings
-
self_and_siblings - [self]
-
end
-
-
# Returns all siblings and a reference to the current node.
-
#
-
# subchild1.self_and_siblings # => [subchild1, subchild2]
-
1
def self_and_siblings
-
parent ? parent.children : self.class.roots
-
end
-
end
-
end
-
end
-
end
-
# Copyright (c) 2005 Rick Olson
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining
-
# a copy of this software and associated documentation files (the
-
# "Software"), to deal in the Software without restriction, including
-
# without limitation the rights to use, copy, modify, merge, publish,
-
# distribute, sublicense, and/or sell copies of the Software, and to
-
# permit persons to whom the Software is furnished to do so, subject to
-
# the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be
-
# included in all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-
1
module ActiveRecord #:nodoc:
-
1
module Acts #:nodoc:
-
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
-
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
-
# column is present as well.
-
#
-
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
-
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
-
#
-
# class Page < ActiveRecord::Base
-
# # assumes pages_versions table
-
# acts_as_versioned
-
# end
-
#
-
# Example:
-
#
-
# page = Page.create(:title => 'hello world!')
-
# page.version # => 1
-
#
-
# page.title = 'hello world'
-
# page.save
-
# page.version # => 2
-
# page.versions.size # => 2
-
#
-
# page.revert_to(1) # using version number
-
# page.title # => 'hello world!'
-
#
-
# page.revert_to(page.versions.last) # using versioned instance
-
# page.title # => 'hello world'
-
#
-
# page.versions.earliest # efficient query to find the first version
-
# page.versions.latest # efficient query to find the most recently created version
-
#
-
#
-
# Simple Queries to page between versions
-
#
-
# page.versions.before(version)
-
# page.versions.after(version)
-
#
-
# Access the previous/next versions from the versioned model itself
-
#
-
# version = page.versions.latest
-
# version.previous # go back one version
-
# version.next # go forward one version
-
#
-
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
-
1
module Versioned
-
1
CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
-
1
def self.included(base) # :nodoc:
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# == Configuration options
-
#
-
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
-
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
-
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
-
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
-
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
-
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
-
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
-
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
-
# For finer control, pass either a Proc or modify Model#version_condition_met?
-
#
-
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
-
#
-
# or...
-
#
-
# class Auction
-
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
-
# !expired?
-
# end
-
# end
-
#
-
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
-
# either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
-
# Use this instead if you want to write your own attribute setters (and ignore if_changed):
-
#
-
# def name=(new_name)
-
# write_changed_attribute :name, new_name
-
# end
-
#
-
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
-
# to create an anonymous mixin:
-
#
-
# class Auction
-
# acts_as_versioned do
-
# def started?
-
# !started_at.nil?
-
# end
-
# end
-
# end
-
#
-
# or...
-
#
-
# module AuctionExtension
-
# def started?
-
# !started_at.nil?
-
# end
-
# end
-
# class Auction
-
# acts_as_versioned :extend => AuctionExtension
-
# end
-
#
-
# Example code:
-
#
-
# @auction = Auction.find(1)
-
# @auction.started?
-
# @auction.versions.first.started?
-
#
-
# == Database Schema
-
#
-
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
-
# into a table called #{model}_versions where the model name is singlular. The _versions table should
-
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
-
#
-
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
-
# then that field is reflected in the versioned model as 'versioned_type' by default.
-
#
-
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
-
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
-
#
-
# class AddVersions < ActiveRecord::Migration
-
# def self.up
-
# # create_versioned_table takes the same options hash
-
# # that create_table does
-
# Post.create_versioned_table
-
# end
-
#
-
# def self.down
-
# Post.drop_versioned_table
-
# end
-
# end
-
#
-
# == Changing What Fields Are Versioned
-
#
-
# By default, acts_as_versioned will version all but these fields:
-
#
-
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
-
#
-
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
-
#
-
# class Post < ActiveRecord::Base
-
# acts_as_versioned
-
# self.non_versioned_columns << 'comments_count'
-
# end
-
#
-
1
def acts_as_versioned(options = {}, &extension)
-
# don't allow multiple calls
-
1
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
-
-
1
send :include, ActiveRecord::Acts::Versioned::ActMethods
-
-
1
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
-
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
-
:version_association_options
-
-
# legacy
-
1
alias_method :non_versioned_fields, :non_versioned_columns
-
1
alias_method :non_versioned_fields=, :non_versioned_columns=
-
-
1
class << self
-
1
alias_method :non_versioned_fields, :non_versioned_columns
-
1
alias_method :non_versioned_fields=, :non_versioned_columns=
-
end
-
-
1
send :attr_accessor, :altered_attributes
-
-
1
self.versioned_class_name = options[:class_name] || "Version"
-
1
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
-
1
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
-
1
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
-
1
self.version_column = options[:version_column] || 'version'
-
1
self.version_sequence_name = options[:sequence_name]
-
1
self.max_version_limit = options[:limit].to_i
-
1
self.version_condition = options[:if] || true
-
1
self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
-
1
self.version_association_options = {
-
:class_name => "#{self.to_s}::#{versioned_class_name}",
-
:foreign_key => versioned_foreign_key,
-
:dependent => :delete_all
-
}.merge(options[:association_options] || {})
-
-
1
if block_given?
-
extension_module_name = "#{versioned_class_name}Extension"
-
silence_warnings do
-
self.const_set(extension_module_name, Module.new(&extension))
-
end
-
-
options[:extend] = self.const_get(extension_module_name)
-
end
-
-
1
class_eval do
-
1
has_many :versions, version_association_options do
-
# finds earliest version of this record
-
1
def earliest
-
@earliest ||= find(:first, :order => 'version')
-
end
-
-
# find latest version of this record
-
1
def latest
-
@latest ||= find(:first, :order => 'version desc')
-
end
-
end
-
1
before_save :set_new_version
-
1
after_create :save_version_on_create
-
1
after_update :save_version
-
1
after_save :clear_old_versions
-
1
after_save :clear_altered_attributes
-
-
1
unless options[:if_changed].nil?
-
self.track_altered_attributes = true
-
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
-
options[:if_changed].each do |attr_name|
-
define_method("#{attr_name}=") do |value|
-
write_changed_attribute attr_name, value
-
end
-
end
-
end
-
-
1
include options[:extend] if options[:extend].is_a?(Module)
-
end
-
-
# create the dynamic versioned model
-
1
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
-
1
def self.reloadable? ; false ; end
-
# find first version before the given version
-
1
def self.before(version)
-
find :first, :order => 'version desc',
-
:conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
-
end
-
-
# find first version after the given version.
-
1
def self.after(version)
-
find :first, :order => 'version',
-
:conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
-
end
-
-
1
def previous
-
self.class.before(self)
-
end
-
-
1
def next
-
self.class.after(self)
-
end
-
-
1
def versions_count
-
page.version
-
end
-
end
-
-
1
versioned_class.cattr_accessor :original_class
-
1
versioned_class.original_class = self
-
1
versioned_class.table_name = versioned_table_name
-
1
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
-
:class_name => "::#{self.to_s}",
-
:foreign_key => versioned_foreign_key
-
1
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
-
1
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
-
end
-
end
-
-
1
module ActMethods
-
1
def self.included(base) # :nodoc:
-
1
base.extend ClassMethods
-
end
-
-
# Finds a specific version of this record
-
1
def find_version(version = nil)
-
self.class.find_version(id, version)
-
end
-
-
# Saves a version of the model if applicable
-
1
def save_version
-
save_version_on_create if save_version?
-
end
-
-
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
-
1
def save_version_on_create
-
4
rev = self.class.versioned_class.new
-
4
self.clone_versioned_model(self, rev)
-
4
rev.version = send(self.class.version_column)
-
4
rev.send("#{self.class.versioned_foreign_key}=", self.id)
-
4
rev.save
-
end
-
-
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
-
# Override this method to set your own criteria for clearing old versions.
-
1
def clear_old_versions
-
4
return if self.class.max_version_limit == 0
-
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
-
if excess_baggage > 0
-
sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
-
self.class.versioned_class.connection.execute sql
-
end
-
end
-
-
1
def versions_count
-
version
-
end
-
-
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
-
1
def revert_to(version)
-
if version.is_a?(self.class.versioned_class)
-
return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
-
else
-
return false unless version = versions.find_by_version(version)
-
end
-
self.clone_versioned_model(version, self)
-
self.send("#{self.class.version_column}=", version.version)
-
true
-
end
-
-
# Reverts a model to a given version and saves the model.
-
# Takes either a version number or an instance of the versioned model
-
1
def revert_to!(version)
-
revert_to(version) ? save_without_revision : false
-
end
-
-
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
-
1
def save_without_revision
-
save_without_revision!
-
true
-
rescue
-
false
-
end
-
-
1
def save_without_revision!
-
without_locking do
-
without_revision do
-
save!
-
end
-
end
-
end
-
-
# Returns an array of attribute keys that are versioned. See non_versioned_columns
-
1
def versioned_attributes
-
32
self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
-
end
-
-
# If called with no parameters, gets whether the current model has changed and needs to be versioned.
-
# If called with a single parameter, gets whether the parameter has changed.
-
1
def changed?(attr_name = nil)
-
attr_name.nil? ?
-
(!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
-
(altered_attributes && altered_attributes.include?(attr_name.to_s))
-
end
-
-
# keep old dirty? method
-
1
alias_method :dirty?, :changed?
-
-
# Clones a model. Used when saving a new version or reverting a model's version.
-
1
def clone_versioned_model(orig_model, new_model)
-
4
self.versioned_attributes.each do |key|
-
20
new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
-
end
-
-
4
if self.class.columns_hash.include?(self.class.inheritance_column)
-
if orig_model.is_a?(self.class.versioned_class)
-
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
-
elsif new_model.is_a?(self.class.versioned_class)
-
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
-
end
-
end
-
end
-
-
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
-
1
def save_version?
-
version_condition_met? && changed?
-
end
-
-
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
-
# custom version condition checking.
-
1
def version_condition_met?
-
case
-
when version_condition.is_a?(Symbol)
-
send(version_condition)
-
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
-
version_condition.call(self)
-
else
-
version_condition
-
end
-
end
-
-
# Executes the block with the versioning callbacks disabled.
-
#
-
# @foo.without_revision do
-
# @foo.save
-
# end
-
#
-
1
def without_revision(&block)
-
self.class.without_revision(&block)
-
end
-
-
# Turns off optimistic locking for the duration of the block
-
#
-
# @foo.without_locking do
-
# @foo.save
-
# end
-
#
-
1
def without_locking(&block)
-
self.class.without_locking(&block)
-
end
-
-
1
def empty_callback() end #:nodoc:
-
-
1
protected
-
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
-
1
def set_new_version
-
4
self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
-
end
-
-
# Gets the next available version for the current record, or 1 for a new record
-
1
def next_version
-
4
return 1 if new_record?
-
(versions.calculate(:max, :version) || 0) + 1
-
end
-
-
# clears current changed attributes. Called after save.
-
1
def clear_altered_attributes
-
4
self.altered_attributes = []
-
end
-
-
1
def write_changed_attribute(attr_name, attr_value)
-
# Convert to db type for comparison. Avoids failing Float<=>String comparisons.
-
attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
-
(self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
-
write_attribute(attr_name, attr_value_for_db)
-
end
-
-
1
module ClassMethods
-
# Finds a specific version of a specific row of this model
-
1
def find_version(id, version = nil)
-
return find(id) unless version
-
-
conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
-
options = { :conditions => conditions, :limit => 1 }
-
-
if result = find_versions(id, options).first
-
result
-
else
-
raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
-
end
-
end
-
-
# Finds versions of a specific model. Takes an options hash like <tt>find</tt>
-
1
def find_versions(id, options = {})
-
versioned_class.find :all, {
-
:conditions => ["#{versioned_foreign_key} = ?", id],
-
:order => 'version' }.merge(options)
-
end
-
-
# Returns an array of columns that are versioned. See non_versioned_columns
-
1
def versioned_columns
-
self.columns.select { |c| !non_versioned_columns.include?(c.name) }
-
end
-
-
# Returns an instance of the dynamic versioned model
-
1
def versioned_class
-
8
const_get versioned_class_name
-
end
-
-
# Rake migration task to create the versioned table using options passed to acts_as_versioned
-
1
def create_versioned_table(create_table_options = {})
-
# create version column in main table if it does not exist
-
if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
-
self.connection.add_column table_name, :version, :integer
-
end
-
-
self.connection.create_table(versioned_table_name, create_table_options) do |t|
-
t.column versioned_foreign_key, :integer
-
t.column :version, :integer
-
end
-
-
updated_col = nil
-
self.versioned_columns.each do |col|
-
updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
-
self.connection.add_column versioned_table_name, col.name, col.type,
-
:limit => col.limit,
-
:default => col.default,
-
:scale => col.scale,
-
:precision => col.precision
-
end
-
-
if type_col = self.columns_hash[inheritance_column]
-
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
-
:limit => type_col.limit,
-
:default => type_col.default,
-
:scale => type_col.scale,
-
:precision => type_col.precision
-
end
-
-
if updated_col.nil?
-
self.connection.add_column versioned_table_name, :updated_at, :timestamp
-
end
-
end
-
-
# Rake migration task to drop the versioned table
-
1
def drop_versioned_table
-
self.connection.drop_table versioned_table_name
-
end
-
-
# Executes the block with the versioning callbacks disabled.
-
#
-
# Foo.without_revision do
-
# @foo.save
-
# end
-
#
-
1
def without_revision(&block)
-
class_eval do
-
CALLBACKS.each do |attr_name|
-
alias_method "orig_#{attr_name}".to_sym, attr_name
-
alias_method attr_name, :empty_callback
-
end
-
end
-
block.call
-
ensure
-
class_eval do
-
CALLBACKS.each do |attr_name|
-
alias_method attr_name, "orig_#{attr_name}".to_sym
-
end
-
end
-
end
-
-
# Turns off optimistic locking for the duration of the block
-
#
-
# Foo.without_locking do
-
# @foo.save
-
# end
-
#
-
1
def without_locking(&block)
-
current = ActiveRecord::Base.lock_optimistically
-
ActiveRecord::Base.lock_optimistically = false if current
-
result = block.call
-
ActiveRecord::Base.lock_optimistically = true if current
-
result
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
-
# ActsAsWatchable
-
1
module Redmine
-
1
module Acts
-
1
module Watchable
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def acts_as_watchable(options = {})
-
6
return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
-
6
class_eval do
-
6
has_many :watchers, :as => :watchable, :dependent => :delete_all
-
6
has_many :watcher_users, :through => :watchers, :source => :user, :validate => false
-
-
6
scope :watched_by, lambda { |user_id|
-
{ :include => :watchers,
-
:conditions => ["#{Watcher.table_name}.user_id = ?", user_id] }
-
}
-
6
attr_protected :watcher_ids, :watcher_user_ids
-
end
-
6
send :include, Redmine::Acts::Watchable::InstanceMethods
-
6
alias_method_chain :watcher_user_ids=, :uniq_ids
-
end
-
end
-
-
1
module InstanceMethods
-
1
def self.included(base)
-
6
base.extend ClassMethods
-
end
-
-
# Returns an array of users that are proposed as watchers
-
1
def addable_watcher_users
-
users = self.project.users.sort - self.watcher_users
-
if respond_to?(:visible?)
-
users.reject! {|user| !visible?(user)}
-
end
-
users
-
end
-
-
# Adds user as a watcher
-
1
def add_watcher(user)
-
self.watchers << Watcher.new(:user => user)
-
end
-
-
# Removes user from the watchers list
-
1
def remove_watcher(user)
-
return nil unless user && user.is_a?(User)
-
Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
-
end
-
-
# Adds/removes watcher
-
1
def set_watcher(user, watching=true)
-
watching ? add_watcher(user) : remove_watcher(user)
-
end
-
-
# Overrides watcher_user_ids= to make user_ids uniq
-
1
def watcher_user_ids_with_uniq_ids=(user_ids)
-
2722
if user_ids.is_a?(Array)
-
2722
user_ids = user_ids.uniq
-
end
-
2722
send :watcher_user_ids_without_uniq_ids=, user_ids
-
end
-
-
# Returns true if object is watched by +user+
-
1
def watched_by?(user)
-
10
!!(user && self.watcher_user_ids.detect {|uid| uid == user.id })
-
end
-
-
# Returns an array of watchers' email addresses
-
1
def watcher_recipients
-
1112
notified = watcher_users.active
-
1112
notified.reject! {|user| user.mail_notification == 'none'}
-
-
1112
if respond_to?(:visible?)
-
1112
notified.reject! {|user| !visible?(user)}
-
end
-
1112
notified.collect(&:mail).compact
-
end
-
-
1
module ClassMethods; end
-
end
-
end
-
end
-
end
-
1
require 'awesome_nested_set/awesome_nested_set'
-
1
ActiveRecord::Base.send :extend, CollectiveIdea::Acts::NestedSet
-
-
1
if defined?(ActionView)
-
1
require 'awesome_nested_set/helper'
-
1
ActionView::Base.send :include, CollectiveIdea::Acts::NestedSet::Helper
-
end
-
1
module CollectiveIdea #:nodoc:
-
1
module Acts #:nodoc:
-
1
module NestedSet #:nodoc:
-
-
# This acts provides Nested Set functionality. Nested Set is a smart way to implement
-
# an _ordered_ tree, with the added feature that you can select the children and all of their
-
# descendants with a single query. The drawback is that insertion or move need some complex
-
# sql queries. But everything is done here by this module!
-
#
-
# Nested sets are appropriate each time you want either an orderd tree (menus,
-
# commercial categories) or an efficient way of querying big trees (threaded posts).
-
#
-
# == API
-
#
-
# Methods names are aligned with acts_as_tree as much as possible to make replacment from one
-
# by another easier.
-
#
-
# item.children.create(:name => "child1")
-
#
-
-
# Configuration options are:
-
#
-
# * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
-
# * +:left_column+ - column name for left boundry data, default "lft"
-
# * +:right_column+ - column name for right boundry data, default "rgt"
-
# * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
-
# (if it hasn't been already) and use that as the foreign key restriction. You
-
# can also pass an array to scope by multiple attributes.
-
# Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
-
# * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
-
# child objects are destroyed alongside this object by calling their destroy
-
# method. If set to :delete_all (default), all the child objects are deleted
-
# without calling their destroy method.
-
# * +:counter_cache+ adds a counter cache for the number of children.
-
# defaults to false.
-
# Example: <tt>acts_as_nested_set :counter_cache => :children_count</tt>
-
#
-
# See CollectiveIdea::Acts::NestedSet::Model::ClassMethods for a list of class methods and
-
# CollectiveIdea::Acts::NestedSet::Model::InstanceMethods for a list of instance methods added
-
# to acts_as_nested_set models
-
1
def acts_as_nested_set(options = {})
-
2
options = {
-
:parent_column => 'parent_id',
-
:left_column => 'lft',
-
:right_column => 'rgt',
-
:dependent => :delete_all, # or :destroy
-
:counter_cache => false,
-
:order => 'id'
-
}.merge(options)
-
-
2
if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
-
options[:scope] = "#{options[:scope]}_id".intern
-
end
-
-
2
class_attribute :acts_as_nested_set_options
-
2
self.acts_as_nested_set_options = options
-
-
2
include CollectiveIdea::Acts::NestedSet::Model
-
2
include Columns
-
2
extend Columns
-
-
2
belongs_to :parent, :class_name => self.base_class.to_s,
-
:foreign_key => parent_column_name,
-
:counter_cache => options[:counter_cache],
-
:inverse_of => :children
-
2
has_many :children, :class_name => self.base_class.to_s,
-
:foreign_key => parent_column_name, :order => left_column_name,
-
:inverse_of => :parent,
-
:before_add => options[:before_add],
-
:after_add => options[:after_add],
-
:before_remove => options[:before_remove],
-
:after_remove => options[:after_remove]
-
-
2
attr_accessor :skip_before_destroy
-
-
2
before_create :set_default_left_and_right
-
2
before_save :store_new_parent
-
2
after_save :move_to_new_parent
-
2
before_destroy :destroy_descendants
-
-
# no assignment to structure fields
-
2
[left_column_name, right_column_name].each do |column|
-
4
module_eval <<-"end_eval", __FILE__, __LINE__
-
def #{column}=(x)
-
raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
-
end
-
end_eval
-
end
-
-
2
define_model_callbacks :move
-
end
-
-
1
module Model
-
1
extend ActiveSupport::Concern
-
-
1
module ClassMethods
-
# Returns the first root
-
1
def root
-
roots.first
-
end
-
-
1
def roots
-
where(parent_column_name => nil).order(quoted_left_column_name)
-
end
-
-
1
def leaves
-
where("#{quoted_right_column_name} - #{quoted_left_column_name} = 1").order(quoted_left_column_name)
-
end
-
-
1
def valid?
-
left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
-
end
-
-
1
def left_and_rights_valid?
-
joins("LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
-
"#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}").
-
where(
-
"#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
-
"#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
-
"#{quoted_table_name}.#{quoted_left_column_name} >= " +
-
"#{quoted_table_name}.#{quoted_right_column_name} OR " +
-
"(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
-
"(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
-
"#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
-
).count == 0
-
end
-
-
1
def no_duplicates_for_columns?
-
scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
-
connection.quote_column_name(c)
-
end.push(nil).join(", ")
-
[quoted_left_column_name, quoted_right_column_name].all? do |column|
-
# No duplicates
-
select("#{scope_string}#{column}, COUNT(#{column})").
-
group("#{scope_string}#{column}").
-
having("COUNT(#{column}) > 1").
-
first.nil?
-
end
-
end
-
-
# Wrapper for each_root_valid? that can deal with scope.
-
1
def all_roots_valid?
-
if acts_as_nested_set_options[:scope]
-
roots.group(scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
-
each_root_valid?(grouped_roots)
-
end
-
else
-
each_root_valid?(roots)
-
end
-
end
-
-
1
def each_root_valid?(roots_to_validate)
-
left = right = 0
-
roots_to_validate.all? do |root|
-
(root.left > left && root.right > right).tap do
-
left = root.left
-
right = root.right
-
end
-
end
-
end
-
-
# Rebuilds the left & rights if unset or invalid.
-
# Also very useful for converting from acts_as_tree.
-
1
def rebuild!(validate_nodes = true)
-
# Don't rebuild a valid tree.
-
return true if valid?
-
-
scope = lambda{|node|}
-
if acts_as_nested_set_options[:scope]
-
scope = lambda{|node|
-
scope_column_names.inject(""){|str, column_name|
-
str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
-
}
-
}
-
end
-
indices = {}
-
-
set_left_and_rights = lambda do |node|
-
# set left
-
node[left_column_name] = indices[scope.call(node)] += 1
-
# find
-
where(["#{quoted_parent_column_name} = ? #{scope.call(node)}", node]).order(acts_as_nested_set_options[:order]).each{|n| set_left_and_rights.call(n) }
-
# set right
-
node[right_column_name] = indices[scope.call(node)] += 1
-
node.save!(:validate => validate_nodes)
-
end
-
-
# Find root node(s)
-
root_nodes = where("#{quoted_parent_column_name} IS NULL").order("#{quoted_left_column_name}, #{quoted_right_column_name}, id").each do |root_node|
-
# setup index for this scope
-
indices[scope.call(root_node)] ||= 0
-
set_left_and_rights.call(root_node)
-
end
-
end
-
-
# Iterates over tree elements and determines the current level in the tree.
-
# Only accepts default ordering, odering by an other column than lft
-
# does not work. This method is much more efficent than calling level
-
# because it doesn't require any additional database queries.
-
#
-
# Example:
-
# Category.each_with_level(Category.root.self_and_descendants) do |o, level|
-
#
-
1
def each_with_level(objects)
-
path = [nil]
-
objects.each do |o|
-
if o.parent_id != path.last
-
# we are on a new level, did we decent or ascent?
-
if path.include?(o.parent_id)
-
# remove wrong wrong tailing paths elements
-
path.pop while path.last != o.parent_id
-
else
-
path << o.parent_id
-
end
-
end
-
yield(o, path.length - 1)
-
end
-
end
-
end
-
-
# Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
-
#
-
# category.self_and_descendants.count
-
# category.ancestors.find(:all, :conditions => "name like '%foo%'")
-
1
module InstanceMethods
-
# Value of the parent column
-
1
def parent_id
-
8387
self[parent_column_name]
-
end
-
-
# Value of the left column
-
1
def left
-
14257
self[left_column_name]
-
end
-
-
# Value of the right column
-
1
def right
-
12609
self[right_column_name]
-
end
-
-
# Returns true if this is a root node.
-
1
def root?
-
1347
parent_id.nil?
-
end
-
-
1
def leaf?
-
705
new_record? || (right - left == 1)
-
end
-
-
# Returns true is this is a child node
-
1
def child?
-
564
!parent_id.nil?
-
end
-
-
# Returns root
-
1
def root
-
423
self_and_ancestors.where(parent_column_name => nil).first
-
end
-
-
# Returns the array of all parents and self
-
1
def self_and_ancestors
-
898
nested_set_scope.where([
-
"#{self.class.quoted_table_name}.#{quoted_left_column_name} <= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} >= ?", left, right
-
])
-
end
-
-
# Returns an array of all parents
-
1
def ancestors
-
468
without_self self_and_ancestors
-
end
-
-
# Returns the array of all children of the parent, including self
-
1
def self_and_siblings
-
271
nested_set_scope.where(parent_column_name => parent_id)
-
end
-
-
# Returns the array of all children of the parent, except self
-
1
def siblings
-
271
without_self self_and_siblings
-
end
-
-
# Returns a set of all of its nested children which do not have children
-
1
def leaves
-
2231
descendants.where("#{self.class.quoted_table_name}.#{quoted_right_column_name} - #{self.class.quoted_table_name}.#{quoted_left_column_name} = 1")
-
end
-
-
# Returns the level of this object in the tree
-
# root level is 0
-
1
def level
-
parent_id.nil? ? 0 : ancestors.count
-
end
-
-
# Returns a set of itself and all of its nested children
-
1
def self_and_descendants
-
4460
nested_set_scope.where([
-
"#{self.class.quoted_table_name}.#{quoted_left_column_name} >= ? AND #{self.class.quoted_table_name}.#{quoted_right_column_name} <= ?", left, right
-
])
-
end
-
-
# Returns a set of all of its children and nested children
-
1
def descendants
-
4407
without_self self_and_descendants
-
end
-
-
1
def is_descendant_of?(other)
-
1100
other.left < self.left && self.left < other.right && same_scope?(other)
-
end
-
-
1
def is_or_is_descendant_of?(other)
-
other.left <= self.left && self.left < other.right && same_scope?(other)
-
end
-
-
1
def is_ancestor_of?(other)
-
108
self.left < other.left && other.left < self.right && same_scope?(other)
-
end
-
-
1
def is_or_is_ancestor_of?(other)
-
self.left <= other.left && other.left < self.right && same_scope?(other)
-
end
-
-
# Check if other model is in the same scope
-
1
def same_scope?(other)
-
620
Array(acts_as_nested_set_options[:scope]).all? do |attr|
-
194
self.send(attr) == other.send(attr)
-
end
-
end
-
-
# Find the first sibling to the left
-
1
def left_sibling
-
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} < ?", left]).
-
order("#{self.class.quoted_table_name}.#{quoted_left_column_name} DESC").last
-
end
-
-
# Find the first sibling to the right
-
1
def right_sibling
-
138
siblings.where(["#{self.class.quoted_table_name}.#{quoted_left_column_name} > ?", left]).first
-
end
-
-
# Shorthand method for finding the left sibling and moving to the left of it.
-
1
def move_left
-
move_to_left_of left_sibling
-
end
-
-
# Shorthand method for finding the right sibling and moving to the right of it.
-
1
def move_right
-
move_to_right_of right_sibling
-
end
-
-
# Move the node to the left of another node (you can pass id only)
-
1
def move_to_left_of(node)
-
move_to node, :left
-
end
-
-
# Move the node to the left of another node (you can pass id only)
-
1
def move_to_right_of(node)
-
12
move_to node, :right
-
end
-
-
# Move the node to the child of another node (you can pass id only)
-
1
def move_to_child_of(node)
-
158
move_to node, :child
-
end
-
-
# Move the node to root nodes
-
1
def move_to_root
-
move_to nil, :root
-
end
-
-
1
def move_possible?(target)
-
self != target && # Can't target self
-
250
same_scope?(target) && # can't be in different scopes
-
# !(left..right).include?(target.left..target.right) # this needs tested more
-
# detect impossible move
-
250
!((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
-
end
-
-
1
def to_text
-
self_and_descendants.map do |node|
-
"#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
-
end.join("\n")
-
end
-
-
1
protected
-
-
1
def without_self(scope)
-
5146
scope.where(["#{self.class.quoted_table_name}.#{self.class.primary_key} != ?", self])
-
end
-
-
# All nested set queries should use this nested_set_scope, which performs finds on
-
# the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
-
# declaration.
-
1
def nested_set_scope(options = {})
-
10449
options = {:order => "#{self.class.quoted_table_name}.#{quoted_left_column_name}"}.merge(options)
-
10449
scopes = Array(acts_as_nested_set_options[:scope])
-
options[:conditions] = scopes.inject({}) do |conditions,attr|
-
9816
conditions.merge attr => self[attr]
-
10449
end unless scopes.empty?
-
10449
self.class.base_class.scoped options
-
end
-
-
1
def store_new_parent
-
1767
@move_to_new_parent_id = send("#{parent_column_name}_changed?") ? parent_id : false
-
1767
true # force callback to return true
-
end
-
-
1
def move_to_new_parent
-
1767
if @move_to_new_parent_id.nil?
-
move_to_root
-
1767
elsif @move_to_new_parent_id
-
move_to_child_of(@move_to_new_parent_id)
-
end
-
end
-
-
# on creation, set automatically lft and rgt to the end of the tree
-
1
def set_default_left_and_right
-
2038
highest_right_row = nested_set_scope(:order => "#{quoted_right_column_name} desc").find(:first, :limit => 1,:lock => true )
-
2038
maxright = highest_right_row ? (highest_right_row[right_column_name] || 0) : 0
-
# adds the new node to the right of all existing nodes
-
2038
self[left_column_name] = maxright + 1
-
2038
self[right_column_name] = maxright + 2
-
end
-
-
1
def in_tenacious_transaction(&block)
-
1472
retry_count = 0
-
1472
begin
-
1472
transaction(&block)
-
rescue ActiveRecord::StatementInvalid => error
-
raise unless connection.open_transactions.zero?
-
raise unless error.message =~ /Deadlock found when trying to get lock|Lock wait timeout exceeded/
-
raise unless retry_count < 10
-
retry_count += 1
-
logger.info "Deadlock detected on retry #{retry_count}, restarting transaction"
-
sleep(rand(retry_count)*0.1) # Aloha protocol
-
retry
-
end
-
end
-
-
# Prunes a branch off of the tree, shifting all of the elements on the right
-
# back to the left so the counts still work.
-
1
def destroy_descendants
-
1302
return if right.nil? || left.nil? || skip_before_destroy
-
-
1302
in_tenacious_transaction do
-
1302
reload_nested_set
-
# select the rows in the model that extend past the deletion point and apply a lock
-
1302
self.class.base_class.find(:all,
-
:select => "id",
-
:conditions => ["#{quoted_left_column_name} >= ?", left],
-
:lock => true
-
)
-
-
1302
if acts_as_nested_set_options[:dependent] == :destroy
-
1302
descendants.each do |model|
-
model.skip_before_destroy = true
-
model.destroy
-
end
-
else
-
nested_set_scope.delete_all(
-
["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
-
left, right]
-
)
-
end
-
-
# update lefts and rights for remaining nodes
-
1302
diff = right - left + 1
-
1302
nested_set_scope.update_all(
-
["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
-
["#{quoted_left_column_name} > ?", right]
-
)
-
1302
nested_set_scope.update_all(
-
["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
-
["#{quoted_right_column_name} > ?", right]
-
)
-
-
1302
reload
-
# Don't allow multiple calls to destroy to corrupt the set
-
1302
self.skip_before_destroy = true
-
end
-
end
-
-
# reload left, right, and parent
-
1
def reload_nested_set
-
reload(
-
:select => "#{quoted_left_column_name}, #{quoted_right_column_name}, #{quoted_parent_column_name}",
-
1946
:lock => true
-
)
-
end
-
-
1
def move_to(target, position)
-
170
raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
-
170
run_callbacks :move do
-
170
in_tenacious_transaction do
-
170
if target.is_a? self.class.base_class
-
158
target.reload_nested_set
-
elsif position != :root
-
# load object if node is not an object
-
12
target = nested_set_scope.find(target)
-
end
-
170
self.reload_nested_set
-
-
170
unless position == :root || move_possible?(target)
-
raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
-
end
-
-
170
bound = case position
-
158
when :child; target[right_column_name]
-
when :left; target[left_column_name]
-
12
when :right; target[right_column_name] + 1
-
when :root; 1
-
else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
-
end
-
-
170
if bound > self[right_column_name]
-
bound = bound - 1
-
other_bound = self[right_column_name] + 1
-
else
-
170
other_bound = self[left_column_name] - 1
-
end
-
-
# there would be no change
-
170
return if bound == self[right_column_name] || bound == self[left_column_name]
-
-
# we have defined the boundaries of two non-overlapping intervals,
-
# so sorting puts both the intervals and their boundaries in order
-
158
a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
-
-
# select the rows in the model between a and d, and apply a lock
-
158
self.class.base_class.select('id').lock(true).where(
-
["#{quoted_left_column_name} >= :a and #{quoted_right_column_name} <= :d", {:a => a, :d => d}]
-
)
-
-
158
new_parent = case position
-
158
when :child; target.id
-
when :root; nil
-
else target[parent_column_name]
-
end
-
-
158
self.nested_set_scope.update_all([
-
"#{quoted_left_column_name} = CASE " +
-
"WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
-
"THEN #{quoted_left_column_name} + :d - :b " +
-
"WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
-
"THEN #{quoted_left_column_name} + :a - :c " +
-
"ELSE #{quoted_left_column_name} END, " +
-
"#{quoted_right_column_name} = CASE " +
-
"WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
-
"THEN #{quoted_right_column_name} + :d - :b " +
-
"WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
-
"THEN #{quoted_right_column_name} + :a - :c " +
-
"ELSE #{quoted_right_column_name} END, " +
-
"#{quoted_parent_column_name} = CASE " +
-
"WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
-
"ELSE #{quoted_parent_column_name} END",
-
{:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
-
])
-
end
-
158
target.reload_nested_set if target
-
158
self.reload_nested_set
-
end
-
end
-
-
end
-
-
end
-
-
# Mixed into both classes and instances to provide easy access to the column names
-
1
module Columns
-
1
def left_column_name
-
43241
acts_as_nested_set_options[:left_column]
-
end
-
-
1
def right_column_name
-
32086
acts_as_nested_set_options[:right_column]
-
end
-
-
1
def parent_column_name
-
13114
acts_as_nested_set_options[:parent_column]
-
end
-
-
1
def scope_column_names
-
Array(acts_as_nested_set_options[:scope])
-
end
-
-
1
def quoted_left_column_name
-
26436
connection.quote_column_name(left_column_name)
-
end
-
-
1
def quoted_right_column_name
-
16585
connection.quote_column_name(right_column_name)
-
end
-
-
1
def quoted_parent_column_name
-
2262
connection.quote_column_name(parent_column_name)
-
end
-
-
1
def quoted_scope_column_names
-
scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
-
end
-
end
-
-
end
-
end
-
end
-
1
module CollectiveIdea #:nodoc:
-
1
module Acts #:nodoc:
-
1
module NestedSet #:nodoc:
-
# This module provides some helpers for the model classes using acts_as_nested_set.
-
# It is included by default in all views.
-
#
-
1
module Helper
-
# Returns options for select.
-
# You can exclude some items from the tree.
-
# You can pass a block receiving an item and returning the string displayed in the select.
-
#
-
# == Params
-
# * +class_or_item+ - Class name or top level times
-
# * +mover+ - The item that is being move, used to exlude impossible moves
-
# * +&block+ - a block that will be used to display: {Â |item| ... item.name }
-
#
-
# == Usage
-
#
-
# <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
-
# "#{'–' * i.level} #{i.name}"
-
# }) %>
-
#
-
1
def nested_set_options(class_or_item, mover = nil)
-
if class_or_item.is_a? Array
-
items = class_or_item.reject { |e| !e.root? }
-
else
-
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
-
items = Array(class_or_item)
-
end
-
result = []
-
items.each do |root|
-
result += root.self_and_descendants.map do |i|
-
if mover.nil? || mover.new_record? || mover.move_possible?(i)
-
[yield(i), i.id]
-
end
-
end.compact
-
end
-
result
-
end
-
-
end
-
end
-
end
-
end
-
1
module ActionController
-
# === Action Pack pagination for Active Record collections
-
#
-
# The Pagination module aids in the process of paging large collections of
-
# Active Record objects. It offers macro-style automatic fetching of your
-
# model for multiple views, or explicit fetching for single actions. And if
-
# the magic isn't flexible enough for your needs, you can create your own
-
# paginators with a minimal amount of code.
-
#
-
# The Pagination module can handle as much or as little as you wish. In the
-
# controller, have it automatically query your model for pagination; or,
-
# if you prefer, create Paginator objects yourself.
-
#
-
# Pagination is included automatically for all controllers.
-
#
-
# For help rendering pagination links, see
-
# ActionView::Helpers::PaginationHelper.
-
#
-
# ==== Automatic pagination for every action in a controller
-
#
-
# class PersonController < ApplicationController
-
# model :person
-
#
-
# paginate :people, :order => 'last_name, first_name',
-
# :per_page => 20
-
#
-
# # ...
-
# end
-
#
-
# Each action in this controller now has access to a <tt>@people</tt>
-
# instance variable, which is an ordered collection of model objects for the
-
# current page (at most 20, sorted by last name and first name), and a
-
# <tt>@person_pages</tt> Paginator instance. The current page is determined
-
# by the <tt>params[:page]</tt> variable.
-
#
-
# ==== Pagination for a single action
-
#
-
# def list
-
# @person_pages, @people =
-
# paginate :people, :order => 'last_name, first_name'
-
# end
-
#
-
# Like the previous example, but explicitly creates <tt>@person_pages</tt>
-
# and <tt>@people</tt> for a single action, and uses the default of 10 items
-
# per page.
-
#
-
# ==== Custom/"classic" pagination
-
#
-
# def list
-
# @person_pages = Paginator.new self, Person.count, 10, params[:page]
-
# @people = Person.find :all, :order => 'last_name, first_name',
-
# :limit => @person_pages.items_per_page,
-
# :offset => @person_pages.current.offset
-
# end
-
#
-
# Explicitly creates the paginator from the previous example and uses
-
# Paginator#to_sql to retrieve <tt>@people</tt> from the model.
-
#
-
1
module Pagination
-
1
unless const_defined?(:OPTIONS)
-
# A hash holding options for controllers using macro-style pagination
-
1
OPTIONS = Hash.new
-
-
# The default options for pagination
-
1
DEFAULT_OPTIONS = {
-
:class_name => nil,
-
:singular_name => nil,
-
:per_page => 10,
-
:conditions => nil,
-
:order_by => nil,
-
:order => nil,
-
:join => nil,
-
:joins => nil,
-
:count => nil,
-
:include => nil,
-
:select => nil,
-
:group => nil,
-
:parameter => 'page'
-
}
-
else
-
DEFAULT_OPTIONS[:group] = nil
-
end
-
-
1
def self.included(base) #:nodoc:
-
1
super
-
1
base.extend(ClassMethods)
-
end
-
-
1
def self.validate_options!(collection_id, options, in_action) #:nodoc:
-
options.merge!(DEFAULT_OPTIONS) {|key, old, new| old}
-
-
valid_options = DEFAULT_OPTIONS.keys
-
valid_options << :actions unless in_action
-
-
unknown_option_keys = options.keys - valid_options
-
raise ActionController::ActionControllerError,
-
"Unknown options: #{unknown_option_keys.join(', ')}" unless
-
unknown_option_keys.empty?
-
-
options[:singular_name] ||= ActiveSupport::Inflector.singularize(collection_id.to_s)
-
options[:class_name] ||= ActiveSupport::Inflector.camelize(options[:singular_name])
-
end
-
-
# Returns a paginator and a collection of Active Record model instances
-
# for the paginator's current page. This is designed to be used in a
-
# single action; to automatically paginate multiple actions, consider
-
# ClassMethods#paginate.
-
#
-
# +options+ are:
-
# <tt>:singular_name</tt>:: the singular name to use, if it can't be inferred by singularizing the collection name
-
# <tt>:class_name</tt>:: the class name to use, if it can't be inferred by
-
# camelizing the singular name
-
# <tt>:per_page</tt>:: the maximum number of items to include in a
-
# single page. Defaults to 10
-
# <tt>:conditions</tt>:: optional conditions passed to Model.find(:all, *params) and
-
# Model.count
-
# <tt>:order</tt>:: optional order parameter passed to Model.find(:all, *params)
-
# <tt>:order_by</tt>:: (deprecated, used :order) optional order parameter passed to Model.find(:all, *params)
-
# <tt>:joins</tt>:: optional joins parameter passed to Model.find(:all, *params)
-
# and Model.count
-
# <tt>:join</tt>:: (deprecated, used :joins or :include) optional join parameter passed to Model.find(:all, *params)
-
# and Model.count
-
# <tt>:include</tt>:: optional eager loading parameter passed to Model.find(:all, *params)
-
# and Model.count
-
# <tt>:select</tt>:: :select parameter passed to Model.find(:all, *params)
-
#
-
# <tt>:count</tt>:: parameter passed as :select option to Model.count(*params)
-
#
-
# <tt>:group</tt>:: :group parameter passed to Model.find(:all, *params). It forces the use of DISTINCT instead of plain COUNT to come up with the total number of records
-
#
-
1
def paginate(collection_id, options={})
-
Pagination.validate_options!(collection_id, options, true)
-
paginator_and_collection_for(collection_id, options)
-
end
-
-
# These methods become class methods on any controller
-
1
module ClassMethods
-
# Creates a +before_filter+ which automatically paginates an Active
-
# Record model for all actions in a controller (or certain actions if
-
# specified with the <tt>:actions</tt> option).
-
#
-
# +options+ are the same as PaginationHelper#paginate, with the addition
-
# of:
-
# <tt>:actions</tt>:: an array of actions for which the pagination is
-
# active. Defaults to +nil+ (i.e., every action)
-
1
def paginate(collection_id, options={})
-
Pagination.validate_options!(collection_id, options, false)
-
module_eval do
-
before_filter :create_paginators_and_retrieve_collections
-
OPTIONS[self] ||= Hash.new
-
OPTIONS[self][collection_id] = options
-
end
-
end
-
end
-
-
1
def create_paginators_and_retrieve_collections #:nodoc:
-
Pagination::OPTIONS[self.class].each do |collection_id, options|
-
next unless options[:actions].include? action_name if
-
options[:actions]
-
-
paginator, collection =
-
paginator_and_collection_for(collection_id, options)
-
-
paginator_name = "@#{options[:singular_name]}_pages"
-
self.instance_variable_set(paginator_name, paginator)
-
-
collection_name = "@#{collection_id.to_s}"
-
self.instance_variable_set(collection_name, collection)
-
end
-
end
-
-
# Returns the total number of items in the collection to be paginated for
-
# the +model+ and given +conditions+. Override this method to implement a
-
# custom counter.
-
1
def count_collection_for_pagination(model, options)
-
model.count(:conditions => options[:conditions],
-
:joins => options[:join] || options[:joins],
-
:include => options[:include],
-
:select => (options[:group] ? "DISTINCT #{options[:group]}" : options[:count]))
-
end
-
-
# Returns a collection of items for the given +model+ and +options[conditions]+,
-
# ordered by +options[order]+, for the current page in the given +paginator+.
-
# Override this method to implement a custom finder.
-
1
def find_collection_for_pagination(model, options, paginator)
-
model.find(:all, :conditions => options[:conditions],
-
:order => options[:order_by] || options[:order],
-
:joins => options[:join] || options[:joins], :include => options[:include],
-
:select => options[:select], :limit => options[:per_page],
-
:group => options[:group], :offset => paginator.current.offset)
-
end
-
-
1
protected :create_paginators_and_retrieve_collections,
-
:count_collection_for_pagination,
-
:find_collection_for_pagination
-
-
1
def paginator_and_collection_for(collection_id, options) #:nodoc:
-
klass = options[:class_name].constantize
-
page = params[options[:parameter]]
-
count = count_collection_for_pagination(klass, options)
-
paginator = Paginator.new(self, count, options[:per_page], page)
-
collection = find_collection_for_pagination(klass, options, paginator)
-
-
return paginator, collection
-
end
-
-
1
private :paginator_and_collection_for
-
-
# A class representing a paginator for an Active Record collection.
-
1
class Paginator
-
1
include Enumerable
-
-
# Creates a new Paginator on the given +controller+ for a set of items
-
# of size +item_count+ and having +items_per_page+ items per page.
-
# Raises ArgumentError if items_per_page is out of bounds (i.e., less
-
# than or equal to zero). The page CGI parameter for links defaults to
-
# "page" and can be overridden with +page_parameter+.
-
1
def initialize(controller, item_count, items_per_page, current_page=1)
-
raise ArgumentError, 'must have at least one item per page' if
-
18
items_per_page <= 0
-
-
18
@controller = controller
-
18
@item_count = item_count || 0
-
18
@items_per_page = items_per_page
-
18
@pages = {}
-
-
18
self.current_page = current_page
-
end
-
1
attr_reader :controller, :item_count, :items_per_page
-
-
# Sets the current page number of this paginator. If +page+ is a Page
-
# object, its +number+ attribute is used as the value; if the page does
-
# not belong to this Paginator, an ArgumentError is raised.
-
1
def current_page=(page)
-
18
if page.is_a? Page
-
raise ArgumentError, 'Page/Paginator mismatch' unless
-
page.paginator == self
-
end
-
18
page = page.to_i
-
18
@current_page_number = has_page_number?(page) ? page : 1
-
end
-
-
# Returns a Page object representing this paginator's current page.
-
1
def current_page
-
93
@current_page ||= self[@current_page_number]
-
end
-
1
alias current :current_page
-
-
# Returns a new Page representing the first page in this paginator.
-
1
def first_page
-
30
@first_page ||= self[1]
-
end
-
1
alias first :first_page
-
-
# Returns a new Page representing the last page in this paginator.
-
1
def last_page
-
30
@last_page ||= self[page_count]
-
end
-
1
alias last :last_page
-
-
# Returns the number of pages in this paginator.
-
1
def page_count
-
@page_count ||= @item_count.zero? ? 1 :
-
48
(q,r=@item_count.divmod(@items_per_page); r==0? q : q+1)
-
end
-
-
1
alias length :page_count
-
-
# Returns true if this paginator contains the page of index +number+.
-
1
def has_page_number?(number)
-
66
number >= 1 and number <= page_count
-
end
-
-
# Returns a new Page representing the page with the given index
-
# +number+.
-
1
def [](number)
-
63
@pages[number] ||= Page.new(self, number)
-
end
-
-
# Successively yields all the paginator's pages to the given block.
-
1
def each(&block)
-
page_count.times do |n|
-
yield self[n+1]
-
end
-
end
-
-
# A class representing a single page in a paginator.
-
1
class Page
-
1
include Comparable
-
-
# Creates a new Page for the given +paginator+ with the index
-
# +number+. If +number+ is not in the range of valid page numbers or
-
# is not a number at all, it defaults to 1.
-
1
def initialize(paginator, number)
-
18
@paginator = paginator
-
18
@number = number.to_i
-
18
@number = 1 unless @paginator.has_page_number? @number
-
end
-
1
attr_reader :paginator, :number
-
1
alias to_i :number
-
-
# Compares two Page objects and returns true when they represent the
-
# same page (i.e., their paginators are the same and they have the
-
# same page number).
-
1
def ==(page)
-
30
return false if page.nil?
-
@paginator == page.paginator and
-
30
@number == page.number
-
end
-
-
# Compares two Page objects and returns -1 if the left-hand page comes
-
# before the right-hand page, 0 if the pages are equal, and 1 if the
-
# left-hand page comes after the right-hand page. Raises ArgumentError
-
# if the pages do not belong to the same Paginator object.
-
1
def <=>(page)
-
raise ArgumentError unless @paginator == page.paginator
-
@number <=> page.number
-
end
-
-
# Returns the item offset for the first item in this page.
-
1
def offset
-
33
@paginator.items_per_page * (@number - 1)
-
end
-
-
# Returns the number of the first item displayed.
-
1
def first_item
-
15
offset + 1
-
end
-
-
# Returns the number of the last item displayed.
-
1
def last_item
-
15
[@paginator.items_per_page * @number, @paginator.item_count].min
-
end
-
-
# Returns true if this page is the first page in the paginator.
-
1
def first?
-
15
self == @paginator.first
-
end
-
-
# Returns true if this page is the last page in the paginator.
-
1
def last?
-
15
self == @paginator.last
-
end
-
-
# Returns a new Page object representing the page just before this
-
# page, or nil if this is the first page.
-
1
def previous
-
15
if first? then nil else @paginator[@number - 1] end
-
end
-
-
# Returns a new Page object representing the page just after this
-
# page, or nil if this is the last page.
-
1
def next
-
15
if last? then nil else @paginator[@number + 1] end
-
end
-
-
# Returns a new Window object for this page with the specified
-
# +padding+.
-
1
def window(padding=2)
-
15
Window.new(self, padding)
-
end
-
-
# Returns the limit/offset array for this page.
-
1
def to_sql
-
[@paginator.items_per_page, offset]
-
end
-
-
1
def to_param #:nodoc:
-
@number.to_s
-
end
-
end
-
-
# A class for representing ranges around a given page.
-
1
class Window
-
# Creates a new Window object for the given +page+ with the specified
-
# +padding+.
-
1
def initialize(page, padding=2)
-
15
@paginator = page.paginator
-
15
@page = page
-
15
self.padding = padding
-
end
-
1
attr_reader :paginator, :page
-
-
# Sets the window's padding (the number of pages on either side of the
-
# window page).
-
1
def padding=(padding)
-
15
@padding = padding < 0 ? 0 : padding
-
# Find the beginning and end pages of the window
-
15
@first = @paginator.has_page_number?(@page.number - @padding) ?
-
@paginator[@page.number - @padding] : @paginator.first
-
15
@last = @paginator.has_page_number?(@page.number + @padding) ?
-
@paginator[@page.number + @padding] : @paginator.last
-
end
-
1
attr_reader :padding, :first, :last
-
-
# Returns an array of Page objects in the current window.
-
1
def pages
-
30
(@first.number..@last.number).to_a.collect! {|n| @paginator[n]}
-
end
-
1
alias to_a :pages
-
end
-
end
-
-
end
-
end
-
1
module ActionView
-
1
module Helpers
-
# Provides methods for linking to ActionController::Pagination objects using a simple generator API. You can optionally
-
# also build your links manually using ActionView::Helpers::AssetHelper#link_to like so:
-
#
-
# <%= link_to "Previous page", { :page => paginator.current.previous } if paginator.current.previous %>
-
# <%= link_to "Next page", { :page => paginator.current.next } if paginator.current.next %>
-
1
module PaginationHelper
-
1
unless const_defined?(:DEFAULT_OPTIONS)
-
1
DEFAULT_OPTIONS = {
-
:name => :page,
-
:window_size => 2,
-
:always_show_anchors => true,
-
:link_to_current_page => false,
-
:params => {}
-
}
-
end
-
-
# Creates a basic HTML link bar for the given +paginator+. Links will be created
-
# for the next and/or previous page and for a number of other pages around the current
-
# pages position. The +html_options+ hash is passed to +link_to+ when the links are created.
-
#
-
# ==== Options
-
# <tt>:name</tt>:: the routing name for this paginator
-
# (defaults to +page+)
-
# <tt>:prefix</tt>:: prefix for pagination links
-
# (i.e. Older Pages: 1 2 3 4)
-
# <tt>:suffix</tt>:: suffix for pagination links
-
# (i.e. 1 2 3 4 <- Older Pages)
-
# <tt>:window_size</tt>:: the number of pages to show around
-
# the current page (defaults to <tt>2</tt>)
-
# <tt>:always_show_anchors</tt>:: whether or not the first and last
-
# pages should always be shown
-
# (defaults to +true+)
-
# <tt>:link_to_current_page</tt>:: whether or not the current page
-
# should be linked to (defaults to
-
# +false+)
-
# <tt>:params</tt>:: any additional routing parameters
-
# for page URLs
-
#
-
# ==== Examples
-
# # We'll assume we have a paginator setup in @person_pages...
-
#
-
# pagination_links(@person_pages)
-
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
-
#
-
# pagination_links(@person_pages, :link_to_current_page => true)
-
# # => <a href="/?page=1/">1</a> <a href="/?page=2/">2</a> <a href="/?page=3/">3</a> ... <a href="/?page=10/">10</a>
-
#
-
# pagination_links(@person_pages, :always_show_anchors => false)
-
# # => 1 <a href="/?page=2/">2</a> <a href="/?page=3/">3</a>
-
#
-
# pagination_links(@person_pages, :window_size => 1)
-
# # => 1 <a href="/?page=2/">2</a> ... <a href="/?page=10/">10</a>
-
#
-
# pagination_links(@person_pages, :params => { :viewer => "flash" })
-
# # => 1 <a href="/?page=2&viewer=flash/">2</a> <a href="/?page=3&viewer=flash/">3</a> ...
-
# # <a href="/?page=10&viewer=flash/">10</a>
-
1
def pagination_links(paginator, options={}, html_options={})
-
name = options[:name] || DEFAULT_OPTIONS[:name]
-
params = (options[:params] || DEFAULT_OPTIONS[:params]).clone
-
-
prefix = options[:prefix] || ''
-
suffix = options[:suffix] || ''
-
-
pagination_links_each(paginator, options, prefix, suffix) do |n|
-
params[name] = n
-
link_to(n.to_s, params, html_options)
-
end
-
end
-
-
# Iterate through the pages of a given +paginator+, invoking a
-
# block for each page number that needs to be rendered as a link.
-
#
-
# ==== Options
-
# <tt>:window_size</tt>:: the number of pages to show around
-
# the current page (defaults to +2+)
-
# <tt>:always_show_anchors</tt>:: whether or not the first and last
-
# pages should always be shown
-
# (defaults to +true+)
-
# <tt>:link_to_current_page</tt>:: whether or not the current page
-
# should be linked to (defaults to
-
# +false+)
-
#
-
# ==== Example
-
# # Turn paginated links into an Ajax call
-
# pagination_links_each(paginator, page_options) do |link|
-
# options = { :url => {:action => 'list'}, :update => 'results' }
-
# html_options = { :href => url_for(:action => 'list') }
-
#
-
# link_to_remote(link.to_s, options, html_options)
-
# end
-
1
def pagination_links_each(paginator, options, prefix = nil, suffix = nil)
-
15
options = DEFAULT_OPTIONS.merge(options)
-
15
link_to_current_page = options[:link_to_current_page]
-
15
always_show_anchors = options[:always_show_anchors]
-
-
15
current_page = paginator.current_page
-
15
window_pages = current_page.window(options[:window_size]).pages
-
15
return if window_pages.length <= 1 unless link_to_current_page
-
-
first, last = paginator.first, paginator.last
-
-
html = ''
-
-
html << prefix if prefix
-
-
if always_show_anchors and not (wp_first = window_pages[0]).first?
-
html << yield(first.number)
-
html << ' ... ' if wp_first.number - first.number > 1
-
html << ' '
-
end
-
-
window_pages.each do |page|
-
if current_page == page && !link_to_current_page
-
html << page.number.to_s
-
else
-
html << yield(page.number)
-
end
-
html << ' '
-
end
-
-
if always_show_anchors and not (wp_last = window_pages[-1]).last?
-
html << ' ... ' if last.number - wp_last.number > 1
-
html << yield(last.number)
-
end
-
-
html << suffix if suffix
-
-
html
-
end
-
-
end # PaginationHelper
-
end # Helpers
-
end # ActionView
-
1
require 'digest/md5'
-
1
require 'cgi'
-
-
1
module GravatarHelper
-
-
# These are the options that control the default behavior of the public
-
# methods. They can be overridden during the actual call to the helper,
-
# or you can set them in your environment.rb as such:
-
#
-
# # Allow racier gravatars
-
# GravatarHelper::DEFAULT_OPTIONS[:rating] = 'R'
-
#
-
1
DEFAULT_OPTIONS = {
-
# The URL of a default image to display if the given email address does
-
# not have a gravatar.
-
:default => nil,
-
-
# The default size in pixels for the gravatar image (they're square).
-
:size => 50,
-
-
# The maximum allowed MPAA rating for gravatars. This allows you to
-
# exclude gravatars that may be out of character for your site.
-
:rating => 'PG',
-
-
# The alt text to use in the img tag for the gravatar. Since it's a
-
# decorational picture, the alt text should be empty according to the
-
# XHTML specs.
-
:alt => '',
-
-
# The title text to use for the img tag for the gravatar.
-
:title => '',
-
-
# The class to assign to the img tag for the gravatar.
-
:class => 'gravatar',
-
-
# Whether or not to display the gravatars using HTTPS instead of HTTP
-
:ssl => false,
-
}
-
-
# The methods that will be made available to your views.
-
1
module PublicMethods
-
-
# Return the HTML img tag for the given user's gravatar. Presumes that
-
# the given user object will respond_to "email", and return the user's
-
# email address.
-
1
def gravatar_for(user, options={})
-
gravatar(user.email, options)
-
end
-
-
# Return the HTML img tag for the given email address's gravatar.
-
1
def gravatar(email, options={})
-
src = h(gravatar_url(email, options))
-
options = DEFAULT_OPTIONS.merge(options)
-
[:class, :alt, :title].each { |opt| options[opt] = h(options[opt]) }
-
image_tag src, options
-
end
-
-
# Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,
-
# a secure URL will be used instead. This is required when the gravatar is to be
-
# displayed on a HTTPS site.
-
1
def gravatar_api_url(hash, ssl=false)
-
2
if ssl
-
"https://secure.gravatar.com/avatar/#{hash}"
-
else
-
2
"http://www.gravatar.com/avatar/#{hash}"
-
end
-
end
-
-
# Return the gravatar URL for the given email address.
-
1
def gravatar_url(email, options={})
-
2
email_hash = Digest::MD5.hexdigest(email)
-
2
options = DEFAULT_OPTIONS.merge(options)
-
2
options[:default] = CGI::escape(options[:default]) unless options[:default].nil?
-
2
gravatar_api_url(email_hash, options.delete(:ssl)).tap do |url|
-
2
opts = []
-
2
[:rating, :size, :default].each do |opt|
-
6
unless options[opt].nil?
-
4
value = h(options[opt])
-
4
opts << [opt, value].join('=')
-
end
-
end
-
2
url << "?#{opts.join('&')}" unless opts.empty?
-
end
-
end
-
-
end
-
-
end
-
1
require 'uri'
-
1
require 'openid'
-
1
require 'rack/openid'
-
-
1
module OpenIdAuthentication
-
1
def self.new(app)
-
1
store = OpenIdAuthentication.store
-
1
if store.nil?
-
1
Rails.logger.warn "OpenIdAuthentication.store is nil. Using in-memory store."
-
end
-
-
1
::Rack::OpenID.new(app, OpenIdAuthentication.store)
-
end
-
-
1
def self.store
-
2
@@store
-
end
-
-
1
def self.store=(*store_option)
-
1
store, *parameters = *([ store_option ].flatten)
-
-
1
@@store = case store
-
when :memory
-
require 'openid/store/memory'
-
OpenID::Store::Memory.new
-
when :file
-
require 'openid/store/filesystem'
-
OpenID::Store::Filesystem.new(Rails.root.join('tmp/openids'))
-
when :memcache
-
require 'memcache'
-
require 'openid/store/memcache'
-
OpenID::Store::Memcache.new(MemCache.new(parameters))
-
else
-
1
store
-
end
-
end
-
-
1
self.store = nil
-
-
1
class InvalidOpenId < StandardError
-
end
-
-
1
class Result
-
1
ERROR_MESSAGES = {
-
:missing => "Sorry, the OpenID server couldn't be found",
-
:invalid => "Sorry, but this does not appear to be a valid OpenID",
-
:canceled => "OpenID verification was canceled",
-
:failed => "OpenID verification failed",
-
:setup_needed => "OpenID verification needs setup"
-
}
-
-
1
def self.[](code)
-
new(code)
-
end
-
-
1
def initialize(code)
-
@code = code
-
end
-
-
1
def status
-
@code
-
end
-
-
6
ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
-
-
1
def successful?
-
@code == :successful
-
end
-
-
1
def unsuccessful?
-
ERROR_MESSAGES.keys.include?(@code)
-
end
-
-
1
def message
-
ERROR_MESSAGES[@code]
-
end
-
end
-
-
# normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
-
1
def self.normalize_identifier(identifier)
-
# clean up whitespace
-
identifier = identifier.to_s.strip
-
-
# if an XRI has a prefix, strip it.
-
identifier.gsub!(/xri:\/\//i, '')
-
-
# dodge XRIs -- TODO: validate, don't just skip.
-
unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
-
# does it begin with http? if not, add it.
-
identifier = "http://#{identifier}" unless identifier =~ /^http/i
-
-
# strip any fragments
-
identifier.gsub!(/\#(.*)$/, '')
-
-
begin
-
uri = URI.parse(identifier)
-
uri.scheme = uri.scheme.downcase if uri.scheme # URI should do this
-
identifier = uri.normalize.to_s
-
rescue URI::InvalidURIError
-
raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
-
end
-
end
-
-
return identifier
-
end
-
-
1
protected
-
# The parameter name of "openid_identifier" is used rather than
-
# the Rails convention "open_id_identifier" because that's what
-
# the specification dictates in order to get browser auto-complete
-
# working across sites
-
1
def using_open_id?(identifier = nil) #:doc:
-
identifier ||= open_id_identifier
-
!identifier.blank? || request.env[Rack::OpenID::RESPONSE]
-
end
-
-
1
def authenticate_with_open_id(identifier = nil, options = {}, &block) #:doc:
-
identifier ||= open_id_identifier
-
-
if request.env[Rack::OpenID::RESPONSE]
-
complete_open_id_authentication(&block)
-
else
-
begin_open_id_authentication(identifier, options, &block)
-
end
-
end
-
-
1
private
-
1
def open_id_identifier
-
params[:openid_identifier] || params[:openid_url]
-
end
-
-
1
def begin_open_id_authentication(identifier, options = {})
-
options[:identifier] = identifier
-
value = Rack::OpenID.build_header(options)
-
response.headers[Rack::OpenID::AUTHENTICATE_HEADER] = value
-
head :unauthorized
-
end
-
-
1
def complete_open_id_authentication
-
response = request.env[Rack::OpenID::RESPONSE]
-
identifier = response.display_identifier
-
-
case response.status
-
when OpenID::Consumer::SUCCESS
-
yield Result[:successful], identifier,
-
OpenID::SReg::Response.from_success_response(response)
-
when :missing
-
yield Result[:missing], identifier, nil
-
when :invalid
-
yield Result[:invalid], identifier, nil
-
when OpenID::Consumer::CANCEL
-
yield Result[:canceled], identifier, nil
-
when OpenID::Consumer::FAILURE
-
yield Result[:failed], identifier, nil
-
when OpenID::Consumer::SETUP_NEEDED
-
yield Result[:setup_needed], response.setup_url, nil
-
end
-
end
-
end
-
1
module PrototypeHelper
-
# Creates a button with an onclick event which calls a remote action
-
# via XMLHttpRequest
-
# The options for specifying the target with :url
-
# and defining callbacks is the same as link_to_remote.
-
1
def button_to_remote(name, options = {}, html_options = {})
-
button_to_function(name, remote_function(options), html_options)
-
end
-
-
# Returns a button input tag with the element name of +name+ and a value (i.e., display text) of +value+
-
# that will submit form using XMLHttpRequest in the background instead of a regular POST request that
-
# reloads the page.
-
#
-
# # Create a button that submits to the create action
-
# #
-
# # Generates: <input name="create_btn" onclick="new Ajax.Request('/testing/create',
-
# # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
-
# # return false;" type="button" value="Create" />
-
# <%= submit_to_remote 'create_btn', 'Create', :url => { :action => 'create' } %>
-
#
-
# # Submit to the remote action update and update the DIV succeed or fail based
-
# # on the success or failure of the request
-
# #
-
# # Generates: <input name="update_btn" onclick="new Ajax.Updater({success:'succeed',failure:'fail'},
-
# # '/testing/update', {asynchronous:true, evalScripts:true, parameters:Form.serialize(this.form)});
-
# # return false;" type="button" value="Update" />
-
# <%= submit_to_remote 'update_btn', 'Update', :url => { :action => 'update' },
-
# :update => { :success => "succeed", :failure => "fail" }
-
#
-
# <tt>options</tt> argument is the same as in form_remote_tag.
-
1
def submit_to_remote(name, value, options = {})
-
options[:with] ||= 'Form.serialize(this.form)'
-
-
html_options = options.delete(:html) || {}
-
html_options[:name] = name
-
-
button_to_remote(value, options, html_options)
-
end
-
-
# Returns a link to a remote action defined by <tt>options[:url]</tt>
-
# (using the url_for format) that's called in the background using
-
# XMLHttpRequest. The result of that request can then be inserted into a
-
# DOM object whose id can be specified with <tt>options[:update]</tt>.
-
# Usually, the result would be a partial prepared by the controller with
-
# render :partial.
-
#
-
# Examples:
-
# # Generates: <a href="#" onclick="new Ajax.Updater('posts', '/blog/destroy/3', {asynchronous:true, evalScripts:true});
-
# # return false;">Delete this post</a>
-
# link_to_remote "Delete this post", :update => "posts",
-
# :url => { :action => "destroy", :id => post.id }
-
#
-
# # Generates: <a href="#" onclick="new Ajax.Updater('emails', '/mail/list_emails', {asynchronous:true, evalScripts:true});
-
# # return false;"><img alt="Refresh" src="/images/refresh.png?" /></a>
-
# link_to_remote(image_tag("refresh"), :update => "emails",
-
# :url => { :action => "list_emails" })
-
#
-
# You can override the generated HTML options by specifying a hash in
-
# <tt>options[:html]</tt>.
-
#
-
# link_to_remote "Delete this post", :update => "posts",
-
# :url => post_url(@post), :method => :delete,
-
# :html => { :class => "destructive" }
-
#
-
# You can also specify a hash for <tt>options[:update]</tt> to allow for
-
# easy redirection of output to an other DOM element if a server-side
-
# error occurs:
-
#
-
# Example:
-
# # Generates: <a href="#" onclick="new Ajax.Updater({success:'posts',failure:'error'}, '/blog/destroy/5',
-
# # {asynchronous:true, evalScripts:true}); return false;">Delete this post</a>
-
# link_to_remote "Delete this post",
-
# :url => { :action => "destroy", :id => post.id },
-
# :update => { :success => "posts", :failure => "error" }
-
#
-
# Optionally, you can use the <tt>options[:position]</tt> parameter to
-
# influence how the target DOM element is updated. It must be one of
-
# <tt>:before</tt>, <tt>:top</tt>, <tt>:bottom</tt>, or <tt>:after</tt>.
-
#
-
# The method used is by default POST. You can also specify GET or you
-
# can simulate PUT or DELETE over POST. All specified with <tt>options[:method]</tt>
-
#
-
# Example:
-
# # Generates: <a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'});
-
# # return false;">Destroy</a>
-
# link_to_remote "Destroy", :url => person_url(:id => person), :method => :delete
-
#
-
# By default, these remote requests are processed asynchronous during
-
# which various JavaScript callbacks can be triggered (for progress
-
# indicators and the likes). All callbacks get access to the
-
# <tt>request</tt> object, which holds the underlying XMLHttpRequest.
-
#
-
# To access the server response, use <tt>request.responseText</tt>, to
-
# find out the HTTP status, use <tt>request.status</tt>.
-
#
-
# Example:
-
# # Generates: <a href="#" onclick="new Ajax.Request('/words/undo?n=33', {asynchronous:true, evalScripts:true,
-
# # onComplete:function(request){undoRequestCompleted(request)}}); return false;">hello</a>
-
# word = 'hello'
-
# link_to_remote word,
-
# :url => { :action => "undo", :n => word_counter },
-
# :complete => "undoRequestCompleted(request)"
-
#
-
# The callbacks that may be specified are (in order):
-
#
-
# <tt>:loading</tt>:: Called when the remote document is being
-
# loaded with data by the browser.
-
# <tt>:loaded</tt>:: Called when the browser has finished loading
-
# the remote document.
-
# <tt>:interactive</tt>:: Called when the user can interact with the
-
# remote document, even though it has not
-
# finished loading.
-
# <tt>:success</tt>:: Called when the XMLHttpRequest is completed,
-
# and the HTTP status code is in the 2XX range.
-
# <tt>:failure</tt>:: Called when the XMLHttpRequest is completed,
-
# and the HTTP status code is not in the 2XX
-
# range.
-
# <tt>:complete</tt>:: Called when the XMLHttpRequest is complete
-
# (fires after success/failure if they are
-
# present).
-
#
-
# You can further refine <tt>:success</tt> and <tt>:failure</tt> by
-
# adding additional callbacks for specific status codes.
-
#
-
# Example:
-
# # Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
-
# # on404:function(request){alert('Not found...? Wrong URL...?')},
-
# # onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
-
# link_to_remote word,
-
# :url => { :action => "action" },
-
# 404 => "alert('Not found...? Wrong URL...?')",
-
# :failure => "alert('HTTP Error ' + request.status + '!')"
-
#
-
# A status code callback overrides the success/failure handlers if
-
# present.
-
#
-
# If you for some reason or another need synchronous processing (that'll
-
# block the browser while the request is happening), you can specify
-
# <tt>options[:type] = :synchronous</tt>.
-
#
-
# You can customize further browser side call logic by passing in
-
# JavaScript code snippets via some optional parameters. In their order
-
# of use these are:
-
#
-
# <tt>:confirm</tt>:: Adds confirmation dialog.
-
# <tt>:condition</tt>:: Perform remote request conditionally
-
# by this expression. Use this to
-
# describe browser-side conditions when
-
# request should not be initiated.
-
# <tt>:before</tt>:: Called before request is initiated.
-
# <tt>:after</tt>:: Called immediately after request was
-
# initiated and before <tt>:loading</tt>.
-
# <tt>:submit</tt>:: Specifies the DOM element ID that's used
-
# as the parent of the form elements. By
-
# default this is the current form, but
-
# it could just as well be the ID of a
-
# table row or any other DOM element.
-
# <tt>:with</tt>:: A JavaScript expression specifying
-
# the parameters for the XMLHttpRequest.
-
# Any expressions should return a valid
-
# URL query string.
-
#
-
# Example:
-
#
-
# :with => "'name=' + $('name').value"
-
#
-
# You can generate a link that uses AJAX in the general case, while
-
# degrading gracefully to plain link behavior in the absence of
-
# JavaScript by setting <tt>html_options[:href]</tt> to an alternate URL.
-
# Note the extra curly braces around the <tt>options</tt> hash separate
-
# it as the second parameter from <tt>html_options</tt>, the third.
-
#
-
# Example:
-
# link_to_remote "Delete this post",
-
# { :update => "posts", :url => { :action => "destroy", :id => post.id } },
-
# :href => url_for(:action => "destroy", :id => post.id)
-
1
def link_to_remote(name, options = {}, html_options = nil)
-
20
link_to_function(name, remote_function(options), html_options || options.delete(:html))
-
end
-
-
# Returns a form tag that will submit using XMLHttpRequest in the
-
# background instead of the regular reloading POST arrangement. Even
-
# though it's using JavaScript to serialize the form elements, the form
-
# submission will work just like a regular submission as viewed by the
-
# receiving side (all elements available in <tt>params</tt>). The options for
-
# specifying the target with <tt>:url</tt> and defining callbacks is the same as
-
# +link_to_remote+.
-
#
-
# A "fall-through" target for browsers that doesn't do JavaScript can be
-
# specified with the <tt>:action</tt>/<tt>:method</tt> options on <tt>:html</tt>.
-
#
-
# Example:
-
# # Generates:
-
# # <form action="/some/place" method="post" onsubmit="new Ajax.Request('',
-
# # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)}); return false;">
-
# form_remote_tag :html => { :action =>
-
# url_for(:controller => "some", :action => "place") }
-
#
-
# The Hash passed to the <tt>:html</tt> key is equivalent to the options (2nd)
-
# argument in the FormTagHelper.form_tag method.
-
#
-
# By default the fall-through action is the same as the one specified in
-
# the <tt>:url</tt> (and the default method is <tt>:post</tt>).
-
#
-
# form_remote_tag also takes a block, like form_tag:
-
# # Generates:
-
# # <form action="/" method="post" onsubmit="new Ajax.Request('/',
-
# # {asynchronous:true, evalScripts:true, parameters:Form.serialize(this)});
-
# # return false;"> <div><input name="commit" type="submit" value="Save" /></div>
-
# # </form>
-
# <% form_remote_tag :url => '/posts' do -%>
-
# <div><%= submit_tag 'Save' %></div>
-
# <% end -%>
-
1
def form_remote_tag(options = {}, &block)
-
options[:form] = true
-
-
options[:html] ||= {}
-
options[:html][:onsubmit] =
-
(options[:html][:onsubmit] ? options[:html][:onsubmit] + "; " : "") +
-
"#{remote_function(options)}; return false;"
-
-
form_tag(options[:html].delete(:action) || url_for(options[:url]), options[:html], &block)
-
end
-
-
# Creates a form that will submit using XMLHttpRequest in the background
-
# instead of the regular reloading POST arrangement and a scope around a
-
# specific resource that is used as a base for questioning about
-
# values for the fields.
-
#
-
# === Resource
-
#
-
# Example:
-
# <% remote_form_for(@post) do |f| %>
-
# ...
-
# <% end %>
-
#
-
# This will expand to be the same as:
-
#
-
# <% remote_form_for :post, @post, :url => post_path(@post), :html => { :method => :put, :class => "edit_post", :id => "edit_post_45" } do |f| %>
-
# ...
-
# <% end %>
-
#
-
# === Nested Resource
-
#
-
# Example:
-
# <% remote_form_for([@post, @comment]) do |f| %>
-
# ...
-
# <% end %>
-
#
-
# This will expand to be the same as:
-
#
-
# <% remote_form_for :comment, @comment, :url => post_comment_path(@post, @comment), :html => { :method => :put, :class => "edit_comment", :id => "edit_comment_45" } do |f| %>
-
# ...
-
# <% end %>
-
#
-
# If you don't need to attach a form to a resource, then check out form_remote_tag.
-
#
-
# See FormHelper#form_for for additional semantics.
-
1
def remote_form_for(record_or_name_or_array, *args, &proc)
-
options = args.extract_options!
-
-
case record_or_name_or_array
-
when String, Symbol
-
object_name = record_or_name_or_array
-
when Array
-
object = record_or_name_or_array.last
-
object_name = ActiveModel::Naming.singular(object)
-
apply_form_for_options!(record_or_name_or_array, options)
-
args.unshift object
-
else
-
object = record_or_name_or_array
-
object_name = ActiveModel::Naming.singular(record_or_name_or_array)
-
apply_form_for_options!(object, options)
-
args.unshift object
-
end
-
-
form_remote_tag options do
-
fields_for object_name, *(args << options), &proc
-
end
-
end
-
1
alias_method :form_remote_for, :remote_form_for
-
-
# Returns '<tt>eval(request.responseText)</tt>' which is the JavaScript function
-
# that +form_remote_tag+ can call in <tt>:complete</tt> to evaluate a multiple
-
# update return document using +update_element_function+ calls.
-
1
def evaluate_remote_response
-
"eval(request.responseText)"
-
end
-
-
# Observes the field with the DOM ID specified by +field_id+ and calls a
-
# callback when its contents have changed. The default callback is an
-
# Ajax call. By default the value of the observed field is sent as a
-
# parameter with the Ajax call.
-
#
-
# Example:
-
# # Generates: new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest',
-
# # '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})})
-
# <%= observe_field :suggest, :url => { :action => :find_suggestion },
-
# :frequency => 0.25,
-
# :update => :suggest,
-
# :with => 'q'
-
# %>
-
#
-
# Required +options+ are either of:
-
# <tt>:url</tt>:: +url_for+-style options for the action to call
-
# when the field has changed.
-
# <tt>:function</tt>:: Instead of making a remote call to a URL, you
-
# can specify javascript code to be called instead.
-
# Note that the value of this option is used as the
-
# *body* of the javascript function, a function definition
-
# with parameters named element and value will be generated for you
-
# for example:
-
# observe_field("glass", :frequency => 1, :function => "alert('Element changed')")
-
# will generate:
-
# new Form.Element.Observer('glass', 1, function(element, value) {alert('Element changed')})
-
# The element parameter is the DOM element being observed, and the value is its value at the
-
# time the observer is triggered.
-
#
-
# Additional options are:
-
# <tt>:frequency</tt>:: The frequency (in seconds) at which changes to
-
# this field will be detected. Not setting this
-
# option at all or to a value equal to or less than
-
# zero will use event based observation instead of
-
# time based observation.
-
# <tt>:update</tt>:: Specifies the DOM ID of the element whose
-
# innerHTML should be updated with the
-
# XMLHttpRequest response text.
-
# <tt>:with</tt>:: A JavaScript expression specifying the parameters
-
# for the XMLHttpRequest. The default is to send the
-
# key and value of the observed field. Any custom
-
# expressions should return a valid URL query string.
-
# The value of the field is stored in the JavaScript
-
# variable +value+.
-
#
-
# Examples
-
#
-
# :with => "'my_custom_key=' + value"
-
# :with => "'person[name]=' + prompt('New name')"
-
# :with => "Form.Element.serialize('other-field')"
-
#
-
# Finally
-
# :with => 'name'
-
# is shorthand for
-
# :with => "'name=' + value"
-
# This essentially just changes the key of the parameter.
-
#
-
# Additionally, you may specify any of the options documented in the
-
# <em>Common options</em> section at the top of this document.
-
#
-
# Example:
-
#
-
# # Sends params: {:title => 'Title of the book'} when the book_title input
-
# # field is changed.
-
# observe_field 'book_title',
-
# :url => 'http://example.com/books/edit/1',
-
# :with => 'title'
-
#
-
#
-
1
def observe_field(field_id, options = {})
-
8
if options[:frequency] && options[:frequency] > 0
-
build_observer('Form.Element.Observer', field_id, options)
-
else
-
8
build_observer('Form.Element.EventObserver', field_id, options)
-
end
-
end
-
-
# Observes the form with the DOM ID specified by +form_id+ and calls a
-
# callback when its contents have changed. The default callback is an
-
# Ajax call. By default all fields of the observed field are sent as
-
# parameters with the Ajax call.
-
#
-
# The +options+ for +observe_form+ are the same as the options for
-
# +observe_field+. The JavaScript variable +value+ available to the
-
# <tt>:with</tt> option is set to the serialized form by default.
-
1
def observe_form(form_id, options = {})
-
if options[:frequency]
-
build_observer('Form.Observer', form_id, options)
-
else
-
build_observer('Form.EventObserver', form_id, options)
-
end
-
end
-
-
# Periodically calls the specified url (<tt>options[:url]</tt>) every
-
# <tt>options[:frequency]</tt> seconds (default is 10). Usually used to
-
# update a specified div (<tt>options[:update]</tt>) with the results
-
# of the remote call. The options for specifying the target with <tt>:url</tt>
-
# and defining callbacks is the same as link_to_remote.
-
# Examples:
-
# # Call get_averages and put its results in 'avg' every 10 seconds
-
# # Generates:
-
# # new PeriodicalExecuter(function() {new Ajax.Updater('avg', '/grades/get_averages',
-
# # {asynchronous:true, evalScripts:true})}, 10)
-
# periodically_call_remote(:url => { :action => 'get_averages' }, :update => 'avg')
-
#
-
# # Call invoice every 10 seconds with the id of the customer
-
# # If it succeeds, update the invoice DIV; if it fails, update the error DIV
-
# # Generates:
-
# # new PeriodicalExecuter(function() {new Ajax.Updater({success:'invoice',failure:'error'},
-
# # '/testing/invoice/16', {asynchronous:true, evalScripts:true})}, 10)
-
# periodically_call_remote(:url => { :action => 'invoice', :id => customer.id },
-
# :update => { :success => "invoice", :failure => "error" }
-
#
-
# # Call update every 20 seconds and update the new_block DIV
-
# # Generates:
-
# # new PeriodicalExecuter(function() {new Ajax.Updater('news_block', 'update', {asynchronous:true, evalScripts:true})}, 20)
-
# periodically_call_remote(:url => 'update', :frequency => '20', :update => 'news_block')
-
#
-
1
def periodically_call_remote(options = {})
-
frequency = options[:frequency] || 10 # every ten seconds by default
-
code = "new PeriodicalExecuter(function() {#{remote_function(options)}}, #{frequency})"
-
javascript_tag(code)
-
end
-
-
1
protected
-
1
def build_observer(klass, name, options = {})
-
8
if options[:with] && (options[:with] !~ /[\{=(.]/)
-
options[:with] = "'#{options[:with]}=' + encodeURIComponent(value)"
-
else
-
8
options[:with] ||= 'value' unless options[:function]
-
end
-
-
8
callback = options[:function] || remote_function(options)
-
8
javascript = "new #{klass}('#{name}', "
-
8
javascript << "#{options[:frequency]}, " if options[:frequency]
-
8
javascript << "function(element, value) {"
-
8
javascript << "#{callback}}"
-
8
javascript << ")"
-
8
javascript_tag(javascript)
-
end
-
end
-
-
1
ActionController::Base.helper PrototypeHelper
-
1
module Core::RFPDF
-
1
COLOR_PALETTE = {
-
:black => [0x00, 0x00, 0x00],
-
:white => [0xff, 0xff, 0xff],
-
}.freeze
-
-
# Draw a circle at (<tt>mid_x, mid_y</tt>) with <tt>radius</tt>.
-
#
-
# Options are:
-
# * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
-
# * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
-
# * <tt>:fill_colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_circle(x, y, radius, :border_color => ReportHelper::COLOR_PALETTE[:dark_blue], :border_width => 1)
-
#
-
1
def draw_circle(mid_x, mid_y, radius, options = {})
-
options[:border] ||= 1
-
options[:border_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:border_width] ||= 0.5
-
options[:fill] ||= 1
-
options[:fill_color] ||= Core::RFPDF::COLOR_PALETTE[:white]
-
options[:fill_colorspace] ||= :rgb
-
SetLineWidth(options[:border_width])
-
set_draw_color_a(options[:border_color])
-
set_fill_color_a(options[:fill_color], options[:colorspace])
-
fd = ""
-
fd = "D" if options[:border] == 1
-
fd += "F" if options[:fill] == 1
-
Circle(mid_x, mid_y, radius, fd)
-
end
-
-
# Draw a line from (<tt>x1, y1</tt>) to (<tt>x2, y2</tt>).
-
#
-
# Options are:
-
# * <tt>:line_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:line_width</tt> - Default value is <tt>0.5</tt>.
-
#
-
# Example:
-
#
-
# draw_line(x1, y1, x1, y1+h, :line_color => ReportHelper::COLOR_PALETTE[:dark_blue], :line_width => 1)
-
#
-
1
def draw_line(x1, y1, x2, y2, options = {})
-
options[:line_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:line_width] ||= 0.5
-
set_draw_color_a(options[:line_color])
-
SetLineWidth(options[:line_width])
-
Line(x1, y1, x2, y2)
-
end
-
-
# Draw a string of <tt>text</tt> at (<tt>x, y</tt>).
-
#
-
# Options are:
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is <tt>10</tt>.
-
# * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_text(x, y, header_left, :font_size => 10)
-
#
-
1
def draw_text(x, y, text, options = {})
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font] ||= default_font
-
options[:font_size] ||= 10
-
options[:font_style] ||= ''
-
set_text_color_a(options[:font_color], options[:colorspace])
-
SetFont(options[:font], options[:font_style], options[:font_size])
-
SetXY(x, y)
-
Write(options[:font_size] + 4, text)
-
end
-
-
# Draw a block of <tt>text</tt> at (<tt>x, y</tt>) bounded by <tt>left_margin</tt> and <tt>right_margin_from_right_edge</tt>. Both
-
# margins are measured from their corresponding edge.
-
#
-
# Options are:
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is <tt>10</tt>.
-
# * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_text_block(left_margin, 85, "question", left_margin, 280,
-
# :font_color => ReportHelper::COLOR_PALETTE[:dark_blue],
-
# :font_size => 12,
-
# :font_style => 'I')
-
#
-
1
def draw_text_block(x, y, text, left_margin, right_margin_from_right_edge, options = {})
-
options[:font] ||= default_font
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font_size] ||= 10
-
options[:font_style] ||= ''
-
set_text_color_a(options[:font_color], options[:colorspace])
-
SetFont(options[:font], options[:font_style], options[:font_size])
-
SetXY(x, y)
-
SetLeftMargin(left_margin)
-
SetRightMargin(right_margin_from_right_edge)
-
Write(options[:font_size] + 4, text)
-
SetMargins(0,0,0)
-
end
-
-
# Draw a box at (<tt>x, y</tt>), <tt>w</tt> wide and <tt>h</tt> high.
-
#
-
# Options are:
-
# * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
-
# * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
-
# * <tt>:fill_colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_box(x, y - 1, 38, 22)
-
#
-
1
def draw_box(x, y, w, h, options = {})
-
options[:border] ||= 1
-
options[:border_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:border_width] ||= 0.5
-
options[:fill] ||= 1
-
options[:fill_color] ||= Core::RFPDF::COLOR_PALETTE[:white]
-
options[:fill_colorspace] ||= :rgb
-
SetLineWidth(options[:border_width])
-
set_draw_color_a(options[:border_color])
-
set_fill_color_a(options[:fill_color], options[:fill_colorspace])
-
fd = ""
-
fd = "D" if options[:border] == 1
-
fd += "F" if options[:fill] == 1
-
Rect(x, y, w, h, fd)
-
end
-
-
# Draw a string of <tt>text</tt> at (<tt>x, y</tt>) in a box <tt>w</tt> wide and <tt>h</tt> high.
-
#
-
# Options are:
-
# * <tt>:align</tt> - Vertical alignment 'C' = center, 'L' = left, 'R' = right. Default value is <tt>'C'</tt>.
-
# * <tt>:border</tt> - Draw a border, 0 = no, 1 = yes? Default value is <tt>0</tt>.
-
# * <tt>:border_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:border_width</tt> - Default value is <tt>0.5</tt>.
-
# * <tt>:fill</tt> - Fill the box, 0 = no, 1 = yes? Default value is <tt>1</tt>.
-
# * <tt>:fill_color</tt> - Default value is nothing or <tt>COLOR_PALETTE[:white]</tt>.
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is nothing or <tt>8</tt>.
-
# * <tt>:font_style</tt> - 'B' = bold, 'I' = italic, 'U' = underline. Default value is nothing <tt>''</tt>.
-
# * <tt>:padding</tt> - Default value is nothing or <tt>2</tt>.
-
# * <tt>:x_padding</tt> - Default value is nothing.
-
# * <tt>:valign</tt> - 'M' = middle, 'T' = top, 'B' = bottom. Default value is nothing or <tt>'M'</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_text_box(x, y - 1, 38, 22,
-
# "your_score_title",
-
# :fill => 0,
-
# :font_color => ReportHelper::COLOR_PALETTE[:blue],
-
# :font_line_spacing => 0,
-
# :font_style => "B",
-
# :valign => "M")
-
#
-
1
def draw_text_box(x, y, w, h, text, options = {})
-
options[:align] ||= 'C'
-
options[:border] ||= 0
-
options[:border_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:border_width] ||= 0.5
-
options[:fill] ||= 1
-
options[:fill_color] ||= Core::RFPDF::COLOR_PALETTE[:white]
-
options[:font] ||= default_font
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font_size] ||= 8
-
options[:font_line_spacing] ||= options[:font_size] * 0.3
-
options[:font_style] ||= ''
-
options[:padding] ||= 2
-
options[:x_padding] ||= 0
-
options[:valign] ||= "M"
-
if options[:fill] == 1 or options[:border] == 1
-
draw_box(x, y, w, h, options)
-
end
-
SetMargins(0,0,0)
-
set_text_color_a(options[:font_color], options[:colorspace])
-
font_size = options[:font_size]
-
SetFont(options[:font], options[:font_style], font_size)
-
font_size += options[:font_line_spacing]
-
case options[:valign]
-
when "B", "bottom"
-
y -= options[:padding]
-
when "T", "top"
-
y += options[:padding]
-
end
-
case options[:align]
-
when "L", "left"
-
x += options[:x_padding]
-
w -= options[:x_padding]
-
w -= options[:x_padding]
-
when "R", "right"
-
x += options[:x_padding]
-
w -= options[:x_padding]
-
w -= options[:x_padding]
-
end
-
SetXY(x, y)
-
if GetStringWidth(text) < w or not text["\n"].nil? and (options[:valign] == "T" || options[:valign] == "top")
-
text = text + "\n"
-
end
-
if GetStringWidth(text) > w or not text["\n"].nil? or (options[:valign] == "B" || options[:valign] == "bottom")
-
font_size += options[:font_size] * 0.1
-
# TODO 2006-07-21 Level=1 - this is assuming a 2 line text
-
SetXY(x, y + ((h - (font_size * 2)) / 2)) if (options[:valign] == "M" || options[:valign] == "middle")
-
MultiCell(w, font_size, text, 0, options[:align])
-
else
-
Cell(w, h, text, 0, 0, options[:align])
-
end
-
end
-
-
# Draw a string of <tt>text</tt> at (<tt>x, y</tt>) as a title.
-
#
-
# Options are:
-
# * <tt>:font_color</tt> - Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
# * <tt>:font_size</tt> - Default value is <tt>18</tt>.
-
# * <tt>:font_style</tt> - Default value is nothing or <tt>''</tt>.
-
# * <tt>:colorspace</tt> - Default value is :rgb or <tt>''</tt>.
-
#
-
# Example:
-
#
-
# draw_title(left_margin, 60,
-
# "title:",
-
# :font_color => ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def draw_title(x, y, title, options = {})
-
options[:font_color] ||= Core::RFPDF::COLOR_PALETTE[:black]
-
options[:font] ||= default_font
-
options[:font_size] ||= 18
-
options[:font_style] ||= ''
-
set_text_color_a(options[:font_color], options[:colorspace])
-
SetFont(options[:font], options[:font_style], options[:font_size])
-
SetXY(x, y)
-
Write(options[:font_size] + 2, title)
-
end
-
-
# Set the draw color. Default value is <tt>COLOR_PALETTE[:black]</tt>.
-
#
-
# Example:
-
#
-
# set_draw_color_a(ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def set_draw_color_a(color = Core::RFPDF::COLOR_PALETTE[:black])
-
SetDrawColor(color[0], color[1], color[2])
-
end
-
-
# Set the fill color. Default value is <tt>COLOR_PALETTE[:white]</tt>.
-
#
-
# Example:
-
#
-
# set_fill_color_a(ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def set_fill_color_a(color = Core::RFPDF::COLOR_PALETTE[:white], colorspace = :rgb)
-
if colorspace == :cmyk
-
SetCmykFillColor(color[0], color[1], color[2], color[3])
-
else
-
SetFillColor(color[0], color[1], color[2])
-
end
-
end
-
-
# Set the text color. Default value is <tt>COLOR_PALETTE[:white]</tt>.
-
#
-
# Example:
-
#
-
# set_text_color_a(ReportHelper::COLOR_PALETTE[:dark_blue])
-
#
-
1
def set_text_color_a(color = Core::RFPDF::COLOR_PALETTE[:black], colorspace = :rgb)
-
if colorspace == :cmyk
-
SetCmykTextColor(color[0], color[1], color[2], color[3])
-
else
-
SetTextColor(color[0], color[1], color[2])
-
end
-
end
-
-
# Write a string containing html characters. Default value is <tt>COLOR_PALETTE[:white]</tt>.
-
#
-
# Options are:
-
# * <tt>:height</tt> - Line height. Default value is <tt>20</tt>.
-
#
-
# Example:
-
#
-
# write_html_with_options(html, :height => 12)
-
#
-
#FIXME 2007-08-07 (EJM) Level=0 - This needs to call the TCPDF version.
-
1
def write_html_with_options(html, options = {})
-
options[:fill] ||= 0
-
options[:height] ||= 20
-
options[:new_line_after] ||= false
-
write_html(html, options[:new_line_after], options[:fill], options[:height])
-
return
-
end
-
end
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This implements native php methods used by tcpdf, which have had to be
-
# reimplemented within Ruby.
-
-
1
module RFPDF
-
-
# http://uk2.php.net/getimagesize
-
1
def getimagesize(filename)
-
image = Magick::ImageList.new(filename)
-
-
out = Hash.new
-
out[0] = image.columns
-
out[1] = image.rows
-
-
# These are actually meant to return integer values But I couldn't seem to find anything saying what those values are.
-
# So for now they return strings. The only place that uses this at the moment is the parsejpeg method, so I've changed that too.
-
case image.mime_type
-
when "image/gif"
-
out[2] = "GIF"
-
when "image/jpeg"
-
out[2] = "JPEG"
-
when "image/png"
-
out[2] = "PNG"
-
when " image/vnd.wap.wbmp"
-
out[2] = "WBMP"
-
when "image/x-xpixmap"
-
out[2] = "XPM"
-
end
-
out[3] = "height=\"#{image.rows}\" width=\"#{image.columns}\""
-
out['mime'] = image.mime_type
-
-
# This needs work to cover more situations
-
# I can't see how to just list the number of channels with ImageMagick / rmagick
-
if image.colorspace.to_s == "CMYKColorspace"
-
out['channels'] = 4
-
elsif image.colorspace.to_s == "RGBColorspace"
-
out['channels'] = 3
-
end
-
-
out['bits'] = image.channel_depth
-
File.open( TCPDF.k_path_cache + File::basename(filename), 'w'){|f|
-
f.binmode
-
f.print image.to_blob
-
f.close
-
}
-
-
out
-
end
-
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
# 1.12 contributed by Ed Moss.
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This is direct port of chinese.php
-
#
-
# Chinese PDF support.
-
#
-
# Usage is as follows:
-
#
-
# require 'fpdf'
-
# require 'chinese'
-
# pdf = FPDF.new
-
# pdf.extend(PDF_Chinese)
-
#
-
# This allows it to be combined with other extensions, such as the bookmark
-
# module.
-
-
1
module PDF_Chinese
-
-
1
Big5_widths={' '=>250,'!'=>250,'"'=>408,'#'=>668,'$'=>490,'%'=>875,'&'=>698,'\''=>250,
-
'('=>240,')'=>240,'*'=>417,'+'=>667,','=>250,'-'=>313,'.'=>250,'/'=>520,'0'=>500,'1'=>500,
-
'2'=>500,'3'=>500,'4'=>500,'5'=>500,'6'=>500,'7'=>500,'8'=>500,'9'=>500,':'=>250,';'=>250,
-
'<'=>667,'='=>667,'>'=>667,'?'=>396,'@'=>921,'A'=>677,'B'=>615,'C'=>719,'D'=>760,'E'=>625,
-
'F'=>552,'G'=>771,'H'=>802,'I'=>354,'J'=>354,'K'=>781,'L'=>604,'M'=>927,'N'=>750,'O'=>823,
-
'P'=>563,'Q'=>823,'R'=>729,'S'=>542,'T'=>698,'U'=>771,'V'=>729,'W'=>948,'X'=>771,'Y'=>677,
-
'Z'=>635,'['=>344,'\\'=>520,']'=>344,'^'=>469,'_'=>500,'`'=>250,'a'=>469,'b'=>521,'c'=>427,
-
'd'=>521,'e'=>438,'f'=>271,'g'=>469,'h'=>531,'i'=>250,'j'=>250,'k'=>458,'l'=>240,'m'=>802,
-
'n'=>531,'o'=>500,'p'=>521,'q'=>521,'r'=>365,'s'=>333,'t'=>292,'u'=>521,'v'=>458,'w'=>677,
-
'x'=>479,'y'=>458,'z'=>427,'{'=>480,'|'=>496,'}'=>480,'~'=>667}
-
-
1
GB_widths={' '=>207,'!'=>270,'"'=>342,'#'=>467,'$'=>462,'%'=>797,'&'=>710,'\''=>239,
-
'('=>374,')'=>374,'*'=>423,'+'=>605,','=>238,'-'=>375,'.'=>238,'/'=>334,'0'=>462,'1'=>462,
-
'2'=>462,'3'=>462,'4'=>462,'5'=>462,'6'=>462,'7'=>462,'8'=>462,'9'=>462,':'=>238,';'=>238,
-
'<'=>605,'='=>605,'>'=>605,'?'=>344,'@'=>748,'A'=>684,'B'=>560,'C'=>695,'D'=>739,'E'=>563,
-
'F'=>511,'G'=>729,'H'=>793,'I'=>318,'J'=>312,'K'=>666,'L'=>526,'M'=>896,'N'=>758,'O'=>772,
-
'P'=>544,'Q'=>772,'R'=>628,'S'=>465,'T'=>607,'U'=>753,'V'=>711,'W'=>972,'X'=>647,'Y'=>620,
-
'Z'=>607,'['=>374,'\\'=>333,']'=>374,'^'=>606,'_'=>500,'`'=>239,'a'=>417,'b'=>503,'c'=>427,
-
'd'=>529,'e'=>415,'f'=>264,'g'=>444,'h'=>518,'i'=>241,'j'=>230,'k'=>495,'l'=>228,'m'=>793,
-
'n'=>527,'o'=>524,'p'=>524,'q'=>504,'r'=>338,'s'=>336,'t'=>277,'u'=>517,'v'=>450,'w'=>652,
-
'x'=>466,'y'=>452,'z'=>407,'{'=>370,'|'=>258,'}'=>370,'~'=>605}
-
-
1
def AddCIDFont(family,style,name,cw,cMap,registry)
-
#ActionController::Base::logger.debug registry.to_a.join(":").to_s
-
fontkey=family.downcase+style.upcase
-
unless @fonts[fontkey].nil?
-
Error("Font already added: family style")
-
end
-
i=@fonts.length+1
-
name=name.gsub(' ','')
-
@fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw, 'CMap'=>cMap,'registry'=>registry}
-
end
-
-
1
def AddCIDFonts(family,name,cw,cMap,registry)
-
AddCIDFont(family,'',name,cw,cMap,registry)
-
AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
-
AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
-
AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
-
end
-
-
1
def AddBig5Font(family='Big5',name='MSungStd-Light-Acro')
-
#Add Big5 font with proportional Latin
-
cw=Big5_widths
-
cMap='ETenms-B5-H'
-
registry={'ordering'=>'CNS1','supplement'=>0}
-
#ActionController::Base::logger.debug registry.to_a.join(":").to_s
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddBig5hwFont(family='Big5-hw',name='MSungStd-Light-Acro')
-
#Add Big5 font with half-witdh Latin
-
cw = {}
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='ETen-B5-H'
-
registry={'ordering'=>'CNS1','supplement'=>0}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddGBFont(family='GB',name='STSongStd-Light-Acro')
-
#Add GB font with proportional Latin
-
cw=GB_widths
-
cMap='GBKp-EUC-H'
-
registry={'ordering'=>'GB1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddGBhwFont(family='GB-hw',name='STSongStd-Light-Acro')
-
#Add GB font with half-width Latin
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='GBK-EUC-H'
-
registry={'ordering'=>'GB1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def GetStringWidth(s)
-
if(@current_font['type']=='Type0')
-
return GetMBStringWidth(s)
-
else
-
return super(s)
-
end
-
end
-
-
1
def GetMBStringWidth(s)
-
#Multi-byte version of GetStringWidth()
-
l=0
-
cw=@current_font['cw']
-
nb=s.length
-
i=0
-
while(i<nb)
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
if(c<128)
-
l+=cw[c.chr] if cw[c.chr]
-
i+=1
-
else
-
l+=1000
-
i+=2
-
end
-
end
-
return l*@font_size/1000
-
end
-
-
1
def MultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
if(@current_font['type']=='Type0')
-
MBMultiCell(w,h,txt,border,align,fill,ln)
-
else
-
super(w,h,txt,border,align,fill,ln)
-
end
-
end
-
-
1
def MBMultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
-
#Multi-byte version of MultiCell()
-
cw=@current_font['cw']
-
if(w==0)
-
w=@w-@r_margin-@x
-
end
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
if(nb>0 and s[nb-1]=="\n")
-
nb-=1
-
end
-
b=0
-
if(border)
-
if(border==1)
-
border='LTRB'
-
b='LRT'
-
b2='LR'
-
else
-
b2=''
-
b2='L' unless border.to_s.index('L').nil?
-
b2=b2+'R' unless border.to_s.index('R').nil?
-
b=(border.to_s.index('T')) ? (b2+'T') : b2
-
end
-
end
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
next
-
end
-
if(!ascii)
-
sep=i
-
ls=l
-
elsif(c.chr==' ')
-
sep=i
-
ls=l
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
else
-
Cell(w,h,s[j,sep-j],b,2,align,fill)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(border and not border.to_s.index('B').nil?)
-
b+='B'
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
-
# move cursor to specified position
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x=@l_margin
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
-
1
def Write(h,txt,link='',fill=0)
-
if(@current_font['type']=='Type0')
-
MBWrite(h,txt,link,fill)
-
else
-
super(h,txt,link,fill)
-
end
-
end
-
-
1
def MBWrite(h,txt,link,fill=0)
-
#Multi-byte version of Write()
-
cw=@current_font['cw']
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
next
-
end
-
if(!ascii or c.chr==' ')
-
sep=i
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(@x>@l_margin)
-
#Move to next line
-
@x=@l_margin
-
@y+=h
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
i+=1
-
nl+=1
-
next
-
end
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
else
-
Cell(w,h,s[j,sep-j],0,2,'',fill,link)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(i!=j)
-
Cell(l*@font_size/1000.0,h,s[j,i-j],0,0,'',fill,link)
-
end
-
end
-
-
1
private
-
-
1
def putfonts()
-
nf=@n
-
@diffs.each do |diff|
-
#Encodings
-
newobj()
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
-
out('endobj')
-
end
-
# mqr=get_magic_quotes_runtime()
-
# set_magic_quotes_runtime(0)
-
@font_files.each_pair do |file, info|
-
#Font file embedding
-
newobj()
-
@font_files[file]['n']=@n
-
if(defined('FPDF_FONTPATH'))
-
file=FPDF_FONTPATH+file
-
end
-
size=filesize(file)
-
if(!size)
-
Error('Font file not found')
-
end
-
out('<</Length '+size)
-
if(file[-2]=='.z')
-
out('/Filter /FlateDecode')
-
end
-
out('/Length1 '+info['length1'])
-
unless info['length2'].nil?
-
out('/Length2 '+info['length2']+' /Length3 0')
-
end
-
out('>>')
-
f=fopen(file,'rb')
-
putstream(fread(f,size))
-
fclose(f)
-
out('endobj')
-
end
-
#
-
# set_magic_quotes_runtime(mqr)
-
#
-
@fonts.each_pair do |k, font|
-
#Font objects
-
newobj()
-
@fonts[k]['n']=@n
-
out('<</Type /Font')
-
if(font['type']=='Type0')
-
putType0(font)
-
else
-
name=font['name']
-
out('/BaseFont /'+name)
-
if(font['type']=='core')
-
#Standard font
-
out('/Subtype /Type1')
-
if(name!='Symbol' and name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding')
-
end
-
else
-
#Additional font
-
out('/Subtype /'+font['type'])
-
out('/FirstChar 32')
-
out('/LastChar 255')
-
out('/Widths '+(@n+1)+' 0 R')
-
out('/FontDescriptor '+(@n+2)+' 0 R')
-
if(font['enc'])
-
if !font['diff'].nil?
-
out('/Encoding '+(nf+font['diff'])+' 0 R')
-
else
-
out('/Encoding /WinAnsiEncoding')
-
end
-
end
-
end
-
out('>>')
-
out('endobj')
-
if(font['type']!='core')
-
#Widths
-
newobj()
-
cw=font['cw']
-
s='['
-
32.upto(255) do |i|
-
s+=cw[i.chr]+' '
-
end
-
out(s+']')
-
out('endobj')
-
#Descriptor
-
newobj()
-
s='<</Type /FontDescriptor /FontName /'+name
-
font['desc'].each_pair do |k, v|
-
s+=' /'+k+' '+v
-
end
-
file=font['file']
-
if(file)
-
s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@font_files[file]['n']+' 0 R'
-
end
-
out(s+'>>')
-
out('endobj')
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['CMap'])
-
out('/Encoding /'+font['CMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry '+textstring('Adobe')+' /Ordering '+textstring(font['registry']['ordering'])+' /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
if(font['CMap']=='ETen-B5-H')
-
w='13648 13742 500'
-
elsif(font['CMap']=='GBK-EUC-H')
-
w='814 907 500 7716 [500]'
-
else
-
# ActionController::Base::logger.debug font['cw'].keys.sort.join(' ').to_s
-
# ActionController::Base::logger.debug font['cw'].values.join(' ').to_s
-
w='1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
w +=']'
-
end
-
out('/W ['+w+']>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 50')
-
out('>>')
-
out('endobj')
-
end
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
# 1.12 contributed by Ed Moss.
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This is direct port of japanese.php
-
#
-
# Japanese PDF support.
-
#
-
# Usage is as follows:
-
#
-
# require 'fpdf'
-
# require 'chinese'
-
# pdf = FPDF.new
-
# pdf.extend(PDF_Japanese)
-
#
-
# This allows it to be combined with other extensions, such as the bookmark
-
# module.
-
-
1
module PDF_Japanese
-
-
1
SJIS_widths={' ' => 278, '!' => 299, '"' => 353, '#' => 614, '$' => 614, '%' => 721, '&' => 735, '\'' => 216,
-
'(' => 323, ')' => 323, '*' => 449, '+' => 529, ',' => 219, '-' => 306, '.' => 219, '/' => 453, '0' => 614, '1' => 614,
-
'2' => 614, '3' => 614, '4' => 614, '5' => 614, '6' => 614, '7' => 614, '8' => 614, '9' => 614, ':' => 219, ';' => 219,
-
'<' => 529, '=' => 529, '>' => 529, '?' => 486, '@' => 744, 'A' => 646, 'B' => 604, 'C' => 617, 'D' => 681, 'E' => 567,
-
'F' => 537, 'G' => 647, 'H' => 738, 'I' => 320, 'J' => 433, 'K' => 637, 'L' => 566, 'M' => 904, 'N' => 710, 'O' => 716,
-
'P' => 605, 'Q' => 716, 'R' => 623, 'S' => 517, 'T' => 601, 'U' => 690, 'V' => 668, 'W' => 990, 'X' => 681, 'Y' => 634,
-
'Z' => 578, '[' => 316, '\\' => 614, ']' => 316, '^' => 529, '_' => 500, '`' => 387, 'a' => 509, 'b' => 566, 'c' => 478,
-
'd' => 565, 'e' => 503, 'f' => 337, 'g' => 549, 'h' => 580, 'i' => 275, 'j' => 266, 'k' => 544, 'l' => 276, 'm' => 854,
-
'n' => 579, 'o' => 550, 'p' => 578, 'q' => 566, 'r' => 410, 's' => 444, 't' => 340, 'u' => 575, 'v' => 512, 'w' => 760,
-
'x' => 503, 'y' => 529, 'z' => 453, '{' => 326, '|' => 380, '}' => 326, '~' => 387}
-
-
1
def AddCIDFont(family,style,name,cw,cMap,registry)
-
fontkey=family.downcase+style.upcase
-
unless @fonts[fontkey].nil?
-
Error("CID font already added: family style")
-
end
-
i=@fonts.length+1
-
@fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-120,'ut'=>40,'cw'=>cw,
-
'CMap'=>cMap,'registry'=>registry}
-
end
-
-
1
def AddCIDFonts(family,name,cw,cMap,registry)
-
AddCIDFont(family,'',name,cw,cMap,registry)
-
AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
-
AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
-
AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
-
end
-
-
1
def AddSJISFont(family='SJIS')
-
#Add SJIS font with proportional Latin
-
name='KozMinPro-Regular-Acro'
-
cw=SJIS_widths
-
cMap='90msp-RKSJ-H'
-
registry={'ordering'=>'Japan1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddSJIShwFont(family='SJIS-hw')
-
#Add SJIS font with half-width Latin
-
name='KozMinPro-Regular-Acro'
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='90ms-RKSJ-H'
-
registry={'ordering'=>'Japan1','supplement'=>2}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def GetStringWidth(s)
-
if(@current_font['type']=='Type0')
-
return GetSJISStringWidth(s)
-
else
-
return super(s)
-
end
-
end
-
-
1
def GetSJISStringWidth(s)
-
#SJIS version of GetStringWidth()
-
l=0
-
cw=@current_font['cw']
-
nb=s.length
-
i=0
-
while(i<nb)
-
o = s[i].is_a?(String) ? s[i].ord : s[i]
-
if(o<128)
-
#ASCII
-
l+=cw[o.chr] if cw[o.chr]
-
i+=1
-
elsif(o>=161 and o<=223)
-
#Half-width katakana
-
l+=500
-
i+=1
-
else
-
#Full-width character
-
l+=1000
-
i+=2
-
end
-
end
-
return l*@font_size/1000
-
end
-
-
1
def MultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
if(@current_font['type']=='Type0')
-
SJISMultiCell(w,h,txt,border,align,fill,ln)
-
else
-
super(w,h,txt,border,align,fill,ln)
-
end
-
end
-
-
1
def SJISMultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
-
#Output text with automatic or explicit line breaks
-
cw=@current_font['cw']
-
if(w==0)
-
w=@w-@r_margin-@x
-
end
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
if(nb>0 and s[nb-1]=="\n")
-
nb-=1
-
end
-
b=0
-
if(border)
-
if(border==1)
-
border='LTRB'
-
b='LRT'
-
b2='LR'
-
else
-
b2=''
-
b2='L' unless border.to_s.index('L').nil?
-
b2=b2+'R' unless border.to_s.index('R').nil?
-
b=(border.to_s.index('T')) ? (b2+'T') : b2
-
end
-
end
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
o=c #o=ord(c)
-
if(o==10)
-
#Explicit line break
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
next
-
end
-
if(o<128)
-
#ASCII
-
l+=cw[c.chr] || 0
-
n=1
-
if(o==32)
-
sep=i
-
end
-
elsif(o>=161 and o<=223)
-
#Half-width katakana
-
l+=500
-
n=1
-
sep=i
-
else
-
#Full-width character
-
l+=1000
-
n=2
-
sep=i
-
end
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(i==j)
-
i+=n
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
else
-
Cell(w,h,s[j,sep-j],b,2,align,fill)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
else
-
i+=n
-
if(o>=128)
-
sep=i
-
end
-
end
-
end
-
#Last chunk
-
if(border and not border.to_s.index('B').nil?)
-
b+='B'
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
-
# move cursor to specified position
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x=@l_margin
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
-
1
def Write(h,txt,link='',fill=0)
-
if(@current_font['type']=='Type0')
-
SJISWrite(h,txt,link,fill)
-
else
-
super(h,txt,link,fill)
-
end
-
end
-
-
1
def SJISWrite(h,txt,link,fill=0)
-
#SJIS version of Write()
-
cw=@current_font['cw']
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
o=c
-
if(o==10)
-
#Explicit line break
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
#Go to left margin
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
next
-
end
-
if(o<128)
-
#ASCII
-
l+=cw[c.chr] || 0
-
n=1
-
if(o==32)
-
sep=i
-
end
-
elsif(o>=161 and o<=223)
-
#Half-width katakana
-
l+=500
-
n=1
-
sep=i
-
else
-
#Full-width character
-
l+=1000
-
n=2
-
sep=i
-
end
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(@x>@l_margin)
-
#Move to next line
-
@x=@l_margin
-
@y+=h
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
i+=n
-
nl+=1
-
next
-
end
-
if(i==j)
-
i+=n
-
end
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
else
-
Cell(w,h,s[j,sep-j],0,2,'',fill,link)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
else
-
i+=n
-
if(o>=128)
-
sep=i
-
end
-
end
-
end
-
#Last chunk
-
if(i!=j)
-
Cell(l*@font_size/1000.0,h,s[j,i-j],0,0,'',fill,link)
-
end
-
end
-
-
1
private
-
-
1
def putfonts()
-
nf=@n
-
@diffs.each do |diff|
-
#Encodings
-
newobj()
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
-
out('endobj')
-
end
-
# mqr=get_magic_quotes_runtime()
-
# set_magic_quotes_runtime(0)
-
@font_files.each_pair do |file, info|
-
#Font file embedding
-
newobj()
-
@font_files[file]['n']=@n
-
if(defined('FPDF_FONTPATH'))
-
file=FPDF_FONTPATH+file
-
end
-
size=filesize(file)
-
if(!size)
-
Error('Font file not found')
-
end
-
out('<</Length '+size)
-
if(file[-2]=='.z')
-
out('/Filter /FlateDecode')
-
end
-
out('/Length1 '+info['length1'])
-
unless info['length2'].nil?
-
out('/Length2 '+info['length2']+' /Length3 0')
-
end
-
out('>>')
-
f=fopen(file,'rb')
-
putstream(fread(f,size))
-
fclose(f)
-
out('endobj')
-
end
-
# set_magic_quotes_runtime(mqr)
-
@fonts.each_pair do |k, font|
-
#Font objects
-
newobj()
-
@fonts[k]['n']=@n
-
out('<</Type /Font')
-
if(font['type']=='Type0')
-
putType0(font)
-
else
-
name=font['name']
-
out('/BaseFont /'+name)
-
if(font['type']=='core')
-
#Standard font
-
out('/Subtype /Type1')
-
if(name!='Symbol' and name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding')
-
end
-
else
-
#Additional font
-
out('/Subtype /'+font['type'])
-
out('/FirstChar 32')
-
out('/LastChar 255')
-
out('/Widths '+(@n+1)+' 0 R')
-
out('/FontDescriptor '+(@n+2)+' 0 R')
-
if(font['enc'])
-
if !font['diff'].nil?
-
out('/Encoding '+(nf+font['diff'])+' 0 R')
-
else
-
out('/Encoding /WinAnsiEncoding')
-
end
-
end
-
end
-
out('>>')
-
out('endobj')
-
if(font['type']!='core')
-
#Widths
-
newobj()
-
cw=font['cw']
-
s='['
-
32.upto(255) do |i|
-
s+=cw[i.chr]+' '
-
end
-
out(s+']')
-
out('endobj')
-
#Descriptor
-
newobj()
-
s='<</Type /FontDescriptor /FontName /'+name
-
font['desc'].each_pair do |k, v|
-
s+=' /'+k+' '+v
-
end
-
file=font['file']
-
if(file)
-
s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@font_files[file]['n']+' 0 R'
-
end
-
out(s+'>>')
-
out('endobj')
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['CMap'])
-
out('/Encoding /'+font['CMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
w='/W [1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
out(w+'] 231 325 500 631 [500] 326 389 500]')
-
out('>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 60')
-
out('>>')
-
out('endobj')
-
end
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
# 1.12 contributed by Ed Moss.
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
#
-
# This is direct port of korean.php
-
#
-
# Korean PDF support.
-
#
-
# Usage is as follows:
-
#
-
# require 'fpdf'
-
# require 'chinese'
-
# pdf = FPDF.new
-
# pdf.extend(PDF_Korean)
-
#
-
# This allows it to be combined with other extensions, such as the bookmark
-
# module.
-
-
1
module PDF_Korean
-
-
1
UHC_widths={' ' => 333, '!' => 416, '"' => 416, '#' => 833, '$' => 625, '%' => 916, '&' => 833, '\'' => 250,
-
'(' => 500, ')' => 500, '*' => 500, '+' => 833, ',' => 291, '-' => 833, '.' => 291, '/' => 375, '0' => 625, '1' => 625,
-
'2' => 625, '3' => 625, '4' => 625, '5' => 625, '6' => 625, '7' => 625, '8' => 625, '9' => 625, ':' => 333, ';' => 333,
-
'<' => 833, '=' => 833, '>' => 916, '?' => 500, '@' => 1000, 'A' => 791, 'B' => 708, 'C' => 708, 'D' => 750, 'E' => 708,
-
'F' => 666, 'G' => 750, 'H' => 791, 'I' => 375, 'J' => 500, 'K' => 791, 'L' => 666, 'M' => 916, 'N' => 791, 'O' => 750,
-
'P' => 666, 'Q' => 750, 'R' => 708, 'S' => 666, 'T' => 791, 'U' => 791, 'V' => 750, 'W' => 1000, 'X' => 708, 'Y' => 708,
-
'Z' => 666, '[' => 500, '\\' => 375, ']' => 500, '^' => 500, '_' => 500, '`' => 333, 'a' => 541, 'b' => 583, 'c' => 541,
-
'd' => 583, 'e' => 583, 'f' => 375, 'g' => 583, 'h' => 583, 'i' => 291, 'j' => 333, 'k' => 583, 'l' => 291, 'm' => 875,
-
'n' => 583, 'o' => 583, 'p' => 583, 'q' => 583, 'r' => 458, 's' => 541, 't' => 375, 'u' => 583, 'v' => 583, 'w' => 833,
-
'x' => 625, 'y' => 625, 'z' => 500, '{' => 583, '|' => 583, '}' => 583, '~' => 750}
-
-
1
def AddCIDFont(family,style,name,cw,cMap,registry)
-
fontkey=family.downcase+style.upcase
-
unless @fonts[fontkey].nil?
-
Error("Font already added: family style")
-
end
-
i=@fonts.length+1
-
name=name.gsub(' ','')
-
@fonts[fontkey]={'i'=>i,'type'=>'Type0','name'=>name,'up'=>-130,'ut'=>40,'cw'=>cw,
-
'CMap'=>cMap,'registry'=>registry}
-
end
-
-
1
def AddCIDFonts(family,name,cw,cMap,registry)
-
AddCIDFont(family,'',name,cw,cMap,registry)
-
AddCIDFont(family,'B',name+',Bold',cw,cMap,registry)
-
AddCIDFont(family,'I',name+',Italic',cw,cMap,registry)
-
AddCIDFont(family,'BI',name+',BoldItalic',cw,cMap,registry)
-
end
-
-
1
def AddUHCFont(family='UHC',name='HYSMyeongJoStd-Medium-Acro')
-
#Add UHC font with proportional Latin
-
cw=UHC_widths
-
cMap='KSCms-UHC-H'
-
registry={'ordering'=>'Korea1','supplement'=>1}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def AddUHChwFont(family='UHC-hw',name='HYSMyeongJoStd-Medium-Acro')
-
#Add UHC font with half-witdh Latin
-
32.upto(126) do |i|
-
cw[i.chr]=500
-
end
-
cMap='KSCms-UHC-HW-H'
-
registry={'ordering'=>'Korea1','supplement'=>1}
-
AddCIDFonts(family,name,cw,cMap,registry)
-
end
-
-
1
def GetStringWidth(s)
-
if(@current_font['type']=='Type0')
-
return GetMBStringWidth(s)
-
else
-
return super(s)
-
end
-
end
-
-
1
def GetMBStringWidth(s)
-
#Multi-byte version of GetStringWidth()
-
l=0
-
cw=@current_font['cw']
-
nb=s.length
-
i=0
-
while(i<nb)
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
if(c<128)
-
l+=cw[c.chr] if cw[c.chr]
-
i+=1
-
else
-
l+=1000
-
i+=2
-
end
-
end
-
return l*@font_size/1000
-
end
-
-
1
def MultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
if(@current_font['type']=='Type0')
-
MBMultiCell(w,h,txt,border,align,fill,ln)
-
else
-
super(w,h,txt,border,align,fill,ln)
-
end
-
end
-
-
1
def MBMultiCell(w,h,txt,border=0,align='L',fill=0,ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
-
#Multi-byte version of MultiCell()
-
cw=@current_font['cw']
-
if(w==0)
-
w=@w-@r_margin-@x
-
end
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
if(nb>0 and s[nb-1]=="\n")
-
nb-=1
-
end
-
b=0
-
if(border)
-
if(border==1)
-
border='LTRB'
-
b='LRT'
-
b2='LR'
-
else
-
b2=''
-
b2='L' unless border.to_s.index('L').nil?
-
b2=b2+'R' unless border.to_s.index('R').nil?
-
b=(border.to_s.index('T')) ? (b2+'T') : b2
-
end
-
end
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
next
-
end
-
if(!ascii)
-
sep=i
-
ls=l
-
elsif(c.chr==' ')
-
sep=i
-
ls=l
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
else
-
Cell(w,h,s[j,sep-j],b,2,align,fill)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
nl+=1
-
if(border and nl==2)
-
b=b2
-
end
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(border and not border.to_s.index('B').nil?)
-
b+='B'
-
end
-
Cell(w,h,s[j,i-j],b,2,align,fill)
-
-
# move cursor to specified position
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x=@l_margin
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
-
1
def Write(h,txt,link='',fill=0)
-
if(@current_font['type']=='Type0')
-
MBWrite(h,txt,link,fill)
-
else
-
super(h,txt,link,fill)
-
end
-
end
-
-
1
def MBWrite(h,txt,link,fill=0)
-
#Multi-byte version of Write()
-
cw=@current_font['cw']
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
s=txt.gsub("\r",'')
-
nb=s.length
-
sep=-1
-
i=0
-
j=0
-
l=0
-
nl=1
-
while(i<nb)
-
#Get next character
-
c = s[i].is_a?(String) ? s[i].ord : s[i]
-
#Check if ASCII or MB
-
ascii=(c<128)
-
if(c.chr=="\n")
-
#Explicit line break
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
i+=1
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
next
-
end
-
if(!ascii or c.chr==' ')
-
sep=i
-
end
-
l+=(ascii ? cw[c.chr] : 1000) || 0
-
if(l>wmax)
-
#Automatic line break
-
if(sep==-1 or i==j)
-
if(@x>@l_margin)
-
#Move to next line
-
@x=@l_margin
-
@y+=h
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
i+=1
-
nl+=1
-
next
-
end
-
if(i==j)
-
i+=ascii ? 1 : 2
-
end
-
Cell(w,h,s[j,i-j],0,2,'',fill,link)
-
else
-
Cell(w,h,s[j,sep-j],0,2,'',fill,link)
-
i=(s[sep].chr==' ') ? sep+1 : sep
-
end
-
sep=-1
-
j=i
-
l=0
-
if(nl==1)
-
@x=@l_margin
-
w=@w-@r_margin-@x
-
wmax=(w-2*@c_margin)*1000/@font_size
-
end
-
nl+=1
-
else
-
i+=ascii ? 1 : 2
-
end
-
end
-
#Last chunk
-
if(i!=j)
-
Cell(l*@font_size/1000.0,h,s[j,i-j],0,0,'',fill,link)
-
end
-
end
-
-
1
private
-
-
1
def putfonts()
-
nf=@n
-
@diffs.each do |diff|
-
#Encodings
-
newobj()
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences ['+diff+']>>')
-
out('endobj')
-
end
-
# mqr=get_magic_quotes_runtime()
-
# set_magic_quotes_runtime(0)
-
@font_files.each_pair do |file, info|
-
#Font file embedding
-
newobj()
-
@font_files[file]['n']=@n
-
if(defined('FPDF_FONTPATH'))
-
file=FPDF_FONTPATH+file
-
end
-
size=filesize(file)
-
if(!size)
-
Error('Font file not found')
-
end
-
out('<</Length '+size)
-
if(file[-2]=='.z')
-
out('/Filter /FlateDecode')
-
end
-
out('/Length1 '+info['length1'])
-
if(not info['length2'].nil?)
-
out('/Length2 '+info['length2']+' /Length3 0')
-
end
-
out('>>')
-
f=fopen(file,'rb')
-
putstream(fread(f,size))
-
fclose(f)
-
out('endobj')
-
end
-
# set_magic_quotes_runtime(mqr)
-
@fonts.each_pair do |k, font|
-
#Font objects
-
newobj()
-
@fonts[k]['n']=@n
-
out('<</Type /Font')
-
if(font['type']=='Type0')
-
putType0(font)
-
else
-
name=font['name']
-
out('/BaseFont /'+name)
-
if(font['type']=='core')
-
#Standard font
-
out('/Subtype /Type1')
-
if(name!='Symbol' and name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding')
-
end
-
else
-
#Additional font
-
out('/Subtype /'+font['type'])
-
out('/FirstChar 32')
-
out('/LastChar 255')
-
out('/Widths '+(@n+1)+' 0 R')
-
out('/FontDescriptor '+(@n+2)+' 0 R')
-
if(font['enc'])
-
if(not font['diff'].nil?)
-
out('/Encoding '+(nf+font['diff'])+' 0 R')
-
else
-
out('/Encoding /WinAnsiEncoding')
-
end
-
end
-
end
-
out('>>')
-
out('endobj')
-
if(font['type']!='core')
-
#Widths
-
newobj()
-
cw=font['cw']
-
s='['
-
32.upto(255) do |i|
-
s+=cw[i.chr]+' '
-
end
-
out(s+']')
-
out('endobj')
-
#Descriptor
-
newobj()
-
s='<</Type /FontDescriptor /FontName /'+name
-
font['desc'].each_pair do |k, v|
-
s+=' /'+k+' '+v
-
end
-
file=font['file']
-
if(file)
-
s+=' /FontFile'+(font['type']=='Type1' ? '' : '2')+' '+@font_files[file]['n']+' 0 R'
-
end
-
out(s+'>>')
-
out('endobj')
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['CMap'])
-
out('/Encoding /'+font['CMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
if(font['CMap']=='KSCms-UHC-HW-H')
-
w='8094 8190 500'
-
else
-
w='1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
w +=']'
-
end
-
out('/W ['+w+']>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 50')
-
out('>>')
-
out('endobj')
-
end
-
end
-
# Copyright (c) 2006 4ssoM LLC <www.4ssoM.com>
-
#
-
# The MIT License
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
-
1
require 'action_controller'
-
1
require 'action_view'
-
-
1
require 'rfpdf/action_controller'
-
1
require 'rfpdf/action_view'
-
-
1
require 'rfpdf/template_handler/compile_support'
-
-
1
require 'rfpdf/template_handlers/base'
-
-
-
1
class ActionController::Base
-
1
include RFPDF::ActionController
-
end
-
-
1
class ActionView::Base
-
1
include RFPDF::ActionView
-
end
-
1
module RFPDF
-
1
module ActionController
-
-
1
DEFAULT_RFPDF_OPTIONS = {:inline=>true}
-
-
1
def self.included(base)
-
1
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def rfpdf(options)
-
rfpdf_options = breakdown_rfpdf_options options
-
write_inheritable_hash(:rfpdf, rfpdf_options)
-
end
-
-
1
private
-
-
1
def breakdown_rfpdf_options(options)
-
rfpdf_options = options.dup
-
rfpdf_options
-
end
-
end
-
-
1
def rfpdf(options)
-
@rfpdf_options ||= DEFAULT_RFPDF_OPTIONS.dup
-
@rfpdf_options.merge! options
-
end
-
-
-
1
private
-
-
1
def compute_rfpdf_options
-
@rfpdf_options ||= DEFAULT_RFPDF_OPTIONS.dup
-
@rfpdf_options.merge!(self.class.read_inheritable_attribute(:rfpdf) || {}) {|k,o,n| o}
-
@rfpdf_options
-
end
-
end
-
end
-
-
-
1
module RFPDF
-
1
module ActionView
-
-
1
private
-
1
def _rfpdf_compile_setup(dsl_setup = false)
-
compile_support = RFPDF::TemplateHandler::CompileSupport.new(controller)
-
@rfpdf_options = compile_support.options
-
end
-
-
end
-
end
-
-
# Various mathematical calculations extracted from the PDF::Writer for Ruby gem.
-
# - http://rubyforge.org/projects/ruby-pdf
-
# - Copyright 2003 - 2005 Austin Ziegler.
-
# - Licensed under a MIT-style licence.
-
#
-
-
1
module RFPDF::Math
-
1
PI2 = ::Math::PI * 2.0
-
-
# One degree of arc measured in terms of radians.
-
1
DR = PI2 / 360.0
-
# One radian of arc, measured in terms of degrees.
-
1
RD = 360 / PI2
-
# One degree of arc, measured in terms of gradians.
-
1
DG = 400 / 360.0
-
# One gradian of arc, measured in terms of degrees.
-
1
GD = 360 / 400.0
-
# One radian of arc, measured in terms of gradians.
-
1
RG = 400 / PI2
-
# One gradian of arc, measured in terms of radians.
-
1
GR = PI2 / 400.0
-
-
# Truncate the remainder.
-
1
def remt(num, den)
-
num - den * (num / den.to_f).to_i
-
end
-
-
# Wrap radian values within the range of radians (0..PI2).
-
1
def rad2rad(rad)
-
remt(rad, PI2)
-
end
-
-
# Wrap degree values within the range of degrees (0..360).
-
1
def deg2deg(deg)
-
remt(deg, 360)
-
end
-
-
# Wrap gradian values within the range of gradians (0..400).
-
1
def grad2grad(grad)
-
remt(grad, 400)
-
end
-
-
# Convert degrees to radians. The value will be constrained to the
-
# range of radians (0..PI2) unless +wrap+ is false.
-
1
def deg2rad(deg, wrap = true)
-
rad = DR * deg
-
rad = rad2rad(rad) if wrap
-
rad
-
end
-
-
# Convert degrees to gradians. The value will be constrained to the
-
# range of gradians (0..400) unless +wrap+ is false.
-
1
def deg2grad(deg, wrap = true)
-
grad = DG * deg
-
grad = grad2grad(grad) if wrap
-
grad
-
end
-
-
# Convert radians to degrees. The value will be constrained to the
-
# range of degrees (0..360) unless +wrap+ is false.
-
1
def rad2deg(rad, wrap = true)
-
deg = RD * rad
-
deg = deg2deg(deg) if wrap
-
deg
-
end
-
-
# Convert radians to gradians. The value will be constrained to the
-
# range of gradians (0..400) unless +wrap+ is false.
-
1
def rad2grad(rad, wrap = true)
-
grad = RG * rad
-
grad = grad2grad(grad) if wrap
-
grad
-
end
-
-
# Convert gradians to degrees. The value will be constrained to the
-
# range of degrees (0..360) unless +wrap+ is false.
-
1
def grad2deg(grad, wrap = true)
-
deg = GD * grad
-
deg = deg2deg(deg) if wrap
-
deg
-
end
-
-
# Convert gradians to radians. The value will be constrained to the
-
# range of radians (0..PI2) unless +wrap+ is false.
-
1
def grad2rad(grad, wrap = true)
-
rad = GR * grad
-
rad = rad2rad(rad) if wrap
-
rad
-
end
-
end
-
1
module RFPDF
-
1
module TemplateHandler
-
-
1
class CompileSupport
-
# extend ActiveSupport::Memoizable
-
-
1
attr_reader :options
-
-
1
def initialize(controller)
-
@controller = controller
-
@options = pull_options
-
set_headers
-
end
-
-
1
def pull_options
-
@controller.send :compute_rfpdf_options || {}
-
end
-
-
1
def set_headers
-
set_pragma
-
set_cache_control
-
set_content_type
-
set_disposition
-
end
-
-
# TODO: kept around from railspdf-- maybe not needed anymore? should check.
-
1
def ie_request?
-
@controller.request.env['HTTP_USER_AGENT'] =~ /msie/i
-
end
-
# memoize :ie_request?
-
-
# added to make ie happy with ssl pdf's (per naisayer)
-
1
def ssl_request?
-
# @controller.request.env['SERVER_PROTOCOL'].downcase == "https"
-
@controller.request.ssl?
-
end
-
# memoize :ssl_request?
-
-
# TODO: kept around from railspdf-- maybe not needed anymore? should check.
-
1
def set_pragma
-
if ssl_request? && ie_request?
-
@controller.headers['Pragma'] = 'public' # added to make ie ssl pdfs work (per naisayer)
-
else
-
@controller.headers['Pragma'] ||= ie_request? ? 'no-cache' : ''
-
end
-
end
-
-
# TODO: kept around from railspdf-- maybe not needed anymore? should check.
-
1
def set_cache_control
-
if ssl_request? && ie_request?
-
@controller.headers['Cache-Control'] = 'maxage=1' # added to make ie ssl pdfs work (per naisayer)
-
else
-
@controller.headers['Cache-Control'] ||= ie_request? ? 'no-cache, must-revalidate' : ''
-
end
-
end
-
-
1
def set_content_type
-
@controller.response.content_type ||= Mime::PDF
-
end
-
-
1
def set_disposition
-
inline = options[:inline] ? 'inline' : 'attachment'
-
filename = options[:filename] ? "filename=#{options[:filename]}" : nil
-
@controller.headers["Content-Disposition"] = [inline,filename].compact.join(';')
-
end
-
-
end
-
-
end
-
end
-
-
-
-
1
module RFPDF
-
1
module TemplateHandlers
-
# class Base < ::ActionView::TemplateHandlers::ERB
-
-
# def compile(template)
-
# src = "_rfpdf_compile_setup;" + super
-
# end
-
# end
-
end
-
end
-
-
-
#============================================================+
-
# File name : tcpdf.rb
-
# Begin : 2002-08-03
-
# Last Update : 2007-03-20
-
# Author : Nicola Asuni
-
# Version : 1.53.0.TC031
-
# License : GNU LGPL (http://www.gnu.org/copyleft/lesser.html)
-
#
-
# Description : This is a Ruby class for generating PDF files
-
# on-the-fly without requiring external
-
# extensions.
-
#
-
# IMPORTANT:
-
# This class is an extension and improvement of the Public Domain
-
# FPDF class by Olivier Plathey (http://www.fpdf.org).
-
#
-
# Main changes by Nicola Asuni:
-
# Ruby porting;
-
# UTF-8 Unicode support;
-
# code refactoring;
-
# source code clean up;
-
# code style and formatting;
-
# source code documentation using phpDocumentor (www.phpdoc.org);
-
# All ISO page formats were included;
-
# image scale factor;
-
# includes methods to parse and printsome XHTML code, supporting the following elements: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small;
-
# includes a method to print various barcode formats using an improved version of "Generic Barcode Render Class" by Karim Mribti (http://www.mribti.com/barcode/) (require GD library: http://www.boutell.com/gd/);
-
# defines standard Header() and Footer() methods.
-
#
-
# Ported to Ruby by Ed Moss 2007-08-06
-
#
-
#============================================================+
-
-
#
-
# TCPDF Class.
-
# @package com.tecnick.tcpdf
-
#
-
-
1
@@version = "1.53.0.TC031"
-
1
@@fpdf_charwidths = {}
-
-
1
PDF_PRODUCER = 'TCPDF via RFPDF 1.53.0.TC031 (http://tcpdf.sourceforge.net)'
-
-
1
module TCPDFFontDescriptor
-
1
@@descriptors = { 'freesans' => {} }
-
1
@@font_name = 'freesans'
-
-
1
def self.font(font_name)
-
@@descriptors[font_name.gsub(".rb", "")]
-
end
-
-
1
def self.define(font_name = 'freesans')
-
@@descriptors[font_name] ||= {}
-
yield @@descriptors[font_name]
-
end
-
end
-
-
# This is a Ruby class for generating PDF files on-the-fly without requiring external extensions.<br>
-
# This class is an extension and improvement of the FPDF class by Olivier Plathey (http://www.fpdf.org).<br>
-
# This version contains some changes: [porting to Ruby, support for UTF-8 Unicode, code style and formatting, php documentation (www.phpdoc.org), ISO page formats, minor improvements, image scale factor]<br>
-
# TCPDF project (http://tcpdf.sourceforge.net) is based on the Public Domain FPDF class by Olivier Plathey (http://www.fpdf.org).<br>
-
# To add your own TTF fonts please read /fonts/README.TXT
-
# @name TCPDF
-
# @package com.tecnick.tcpdf
-
# @@version 1.53.0.TC031
-
# @author Nicola Asuni
-
# @link http://tcpdf.sourceforge.net
-
# @license http://www.gnu.org/copyleft/lesser.html LGPL
-
#
-
1
class TCPDF
-
1
include RFPDF
-
1
include Core::RFPDF
-
1
include RFPDF::Math
-
-
1
def logger
-
Rails.logger
-
end
-
-
1
cattr_accessor :k_cell_height_ratio
-
1
@@k_cell_height_ratio = 1.25
-
-
1
cattr_accessor :k_blank_image
-
1
@@k_blank_image = ""
-
-
1
cattr_accessor :k_small_ratio
-
1
@@k_small_ratio = 2/3.0
-
-
1
cattr_accessor :k_path_cache
-
1
@@k_path_cache = Rails.root.join('tmp')
-
-
1
cattr_accessor :k_path_url_cache
-
1
@@k_path_url_cache = Rails.root.join('tmp')
-
-
1
cattr_accessor :decoder
-
-
1
attr_accessor :barcode
-
-
1
attr_accessor :buffer
-
-
1
attr_accessor :diffs
-
-
1
attr_accessor :color_flag
-
-
1
attr_accessor :default_table_columns
-
-
1
attr_accessor :max_table_columns
-
-
1
attr_accessor :default_font
-
-
1
attr_accessor :draw_color
-
-
1
attr_accessor :encoding
-
-
1
attr_accessor :fill_color
-
-
1
attr_accessor :fonts
-
-
1
attr_accessor :font_family
-
-
1
attr_accessor :font_files
-
-
1
cattr_accessor :font_path
-
-
1
attr_accessor :font_style
-
-
1
attr_accessor :font_size_pt
-
-
1
attr_accessor :header_width
-
-
1
attr_accessor :header_logo
-
-
1
attr_accessor :header_logo_width
-
-
1
attr_accessor :header_title
-
-
1
attr_accessor :header_string
-
-
1
attr_accessor :images
-
-
1
attr_accessor :img_scale
-
-
1
attr_accessor :in_footer
-
-
1
attr_accessor :is_unicode
-
-
1
attr_accessor :lasth
-
-
1
attr_accessor :links
-
-
1
attr_accessor :list_ordered
-
-
1
attr_accessor :list_count
-
-
1
attr_accessor :li_spacer
-
-
1
attr_accessor :n
-
-
1
attr_accessor :offsets
-
-
1
attr_accessor :orientation_changes
-
-
1
attr_accessor :page
-
-
1
attr_accessor :page_links
-
-
1
attr_accessor :pages
-
-
1
attr_accessor :pdf_version
-
-
1
attr_accessor :prevfill_color
-
-
1
attr_accessor :prevtext_color
-
-
1
attr_accessor :print_header
-
-
1
attr_accessor :print_footer
-
-
1
attr_accessor :state
-
-
1
attr_accessor :tableborder
-
-
1
attr_accessor :tdbegin
-
-
1
attr_accessor :tdwidth
-
-
1
attr_accessor :tdheight
-
-
1
attr_accessor :tdalign
-
-
1
attr_accessor :tdfill
-
-
1
attr_accessor :tempfontsize
-
-
1
attr_accessor :text_color
-
-
1
attr_accessor :underline
-
-
1
attr_accessor :ws
-
-
#
-
# This is the class constructor.
-
# It allows to set up the page format, the orientation and
-
# the measure unit used in all the methods (except for the font sizes).
-
# @since 1.0
-
# @param string :orientation page orientation. Possible values are (case insensitive):<ul><li>P or Portrait (default)</li><li>L or Landscape</li></ul>
-
# @param string :unit User measure unit. Possible values are:<ul><li>pt: point</li><li>mm: millimeter (default)</li><li>cm: centimeter</li><li>in: inch</li></ul><br />A point equals 1/72 of inch, that is to say about 0.35 mm (an inch being 2.54 cm). This is a very common unit in typography; font sizes are expressed in that unit.
-
# @param mixed :format The format used for pages. It can be either one of the following values (case insensitive) or a custom format in the form of a two-element array containing the width and the height (expressed in the unit given by unit).<ul><li>4A0</li><li>2A0</li><li>A0</li><li>A1</li><li>A2</li><li>A3</li><li>A4 (default)</li><li>A5</li><li>A6</li><li>A7</li><li>A8</li><li>A9</li><li>A10</li><li>B0</li><li>B1</li><li>B2</li><li>B3</li><li>B4</li><li>B5</li><li>B6</li><li>B7</li><li>B8</li><li>B9</li><li>B10</li><li>C0</li><li>C1</li><li>C2</li><li>C3</li><li>C4</li><li>C5</li><li>C6</li><li>C7</li><li>C8</li><li>C9</li><li>C10</li><li>RA0</li><li>RA1</li><li>RA2</li><li>RA3</li><li>RA4</li><li>SRA0</li><li>SRA1</li><li>SRA2</li><li>SRA3</li><li>SRA4</li><li>LETTER</li><li>LEGAL</li><li>EXECUTIVE</li><li>FOLIO</li></ul>
-
# @param boolean :unicode TRUE means that the input text is unicode (default = true)
-
# @param String :encoding charset encoding; default is UTF-8
-
#
-
1
def initialize(orientation = 'P', unit = 'mm', format = 'A4', unicode = true, encoding = "UTF-8")
-
-
# Set internal character encoding to ASCII#
-
#FIXME 2007-05-25 (EJM) Level=0 -
-
# if (respond_to?("mb_internal_encoding") and mb_internal_encoding())
-
# @internal_encoding = mb_internal_encoding();
-
# mb_internal_encoding("ASCII");
-
# }
-
-
#Some checks
-
dochecks();
-
-
begin
-
@@decoder = HTMLEntities.new
-
rescue
-
@@decoder = nil
-
end
-
-
#Initialization of properties
-
@barcode ||= false
-
@buffer ||= ''
-
@diffs ||= []
-
@color_flag ||= false
-
@default_table_columns ||= 4
-
@table_columns ||= 0
-
@max_table_columns ||= []
-
@tr_id ||= 0
-
@max_td_page ||= []
-
@max_td_y ||= []
-
@t_columns ||= 0
-
@default_font ||= "FreeSans" if unicode
-
@default_font ||= "Helvetica"
-
@draw_color ||= '0 G'
-
@encoding ||= "UTF-8"
-
@fill_color ||= '0 g'
-
@fonts ||= {}
-
@font_family ||= ''
-
@font_files ||= {}
-
@font_style ||= ''
-
@font_size ||= 12
-
@font_size_pt ||= 12
-
@header_width ||= 0
-
@header_logo ||= ""
-
@header_logo_width ||= 30
-
@header_title ||= ""
-
@header_string ||= ""
-
@images ||= {}
-
@img_scale ||= 1
-
@in_footer ||= false
-
@is_unicode = unicode
-
@lasth ||= 0
-
@links ||= []
-
@list_ordered ||= []
-
@list_count ||= []
-
@li_spacer ||= ""
-
@li_count ||= 0
-
@spacer ||= ""
-
@quote_count ||= 0
-
@prevquote_count ||= 0
-
@quote_top ||= []
-
@quote_page ||= []
-
@n ||= 2
-
@offsets ||= []
-
@orientation_changes ||= []
-
@page ||= 0
-
@page_links ||= {}
-
@pages ||= []
-
@pdf_version ||= "1.3"
-
@prevfill_color ||= [255,255,255]
-
@prevtext_color ||= [0,0,0]
-
@print_header ||= false
-
@print_footer ||= false
-
@state ||= 0
-
@tableborder ||= 0
-
@tdbegin ||= false
-
@tdwidth ||= 0
-
@tdheight ||= 0
-
@tdalign ||= "L"
-
@tdfill ||= 0
-
@tempfontsize ||= 10
-
@text_color ||= '0 g'
-
@underline ||= false
-
@deleted ||= false
-
@ws ||= 0
-
-
#Standard Unicode fonts
-
@core_fonts = {
-
'courier'=>'Courier',
-
'courierB'=>'Courier-Bold',
-
'courierI'=>'Courier-Oblique',
-
'courierBI'=>'Courier-BoldOblique',
-
'helvetica'=>'Helvetica',
-
'helveticaB'=>'Helvetica-Bold',
-
'helveticaI'=>'Helvetica-Oblique',
-
'helveticaBI'=>'Helvetica-BoldOblique',
-
'times'=>'Times-Roman',
-
'timesB'=>'Times-Bold',
-
'timesI'=>'Times-Italic',
-
'timesBI'=>'Times-BoldItalic',
-
'symbol'=>'Symbol',
-
'zapfdingbats'=>'ZapfDingbats'}
-
-
#Scale factor
-
case unit.downcase
-
when 'pt' ; @k=1
-
when 'mm' ; @k=72/25.4
-
when 'cm' ; @k=72/2.54
-
when 'in' ; @k=72
-
else Error("Incorrect unit: #{unit}")
-
end
-
-
#Page format
-
if format.is_a?(String)
-
# Page formats (45 standard ISO paper formats and 4 american common formats).
-
# Paper cordinates are calculated in this way: (inches# 72) where (1 inch = 2.54 cm)
-
case (format.upcase)
-
when '4A0' ; format = [4767.87,6740.79]
-
when '2A0' ; format = [3370.39,4767.87]
-
when 'A0' ; format = [2383.94,3370.39]
-
when 'A1' ; format = [1683.78,2383.94]
-
when 'A2' ; format = [1190.55,1683.78]
-
when 'A3' ; format = [841.89,1190.55]
-
when 'A4' ; format = [595.28,841.89] # ; default
-
when 'A5' ; format = [419.53,595.28]
-
when 'A6' ; format = [297.64,419.53]
-
when 'A7' ; format = [209.76,297.64]
-
when 'A8' ; format = [147.40,209.76]
-
when 'A9' ; format = [104.88,147.40]
-
when 'A10' ; format = [73.70,104.88]
-
when 'B0' ; format = [2834.65,4008.19]
-
when 'B1' ; format = [2004.09,2834.65]
-
when 'B2' ; format = [1417.32,2004.09]
-
when 'B3' ; format = [1000.63,1417.32]
-
when 'B4' ; format = [708.66,1000.63]
-
when 'B5' ; format = [498.90,708.66]
-
when 'B6' ; format = [354.33,498.90]
-
when 'B7' ; format = [249.45,354.33]
-
when 'B8' ; format = [175.75,249.45]
-
when 'B9' ; format = [124.72,175.75]
-
when 'B10' ; format = [87.87,124.72]
-
when 'C0' ; format = [2599.37,3676.54]
-
when 'C1' ; format = [1836.85,2599.37]
-
when 'C2' ; format = [1298.27,1836.85]
-
when 'C3' ; format = [918.43,1298.27]
-
when 'C4' ; format = [649.13,918.43]
-
when 'C5' ; format = [459.21,649.13]
-
when 'C6' ; format = [323.15,459.21]
-
when 'C7' ; format = [229.61,323.15]
-
when 'C8' ; format = [161.57,229.61]
-
when 'C9' ; format = [113.39,161.57]
-
when 'C10' ; format = [79.37,113.39]
-
when 'RA0' ; format = [2437.80,3458.27]
-
when 'RA1' ; format = [1729.13,2437.80]
-
when 'RA2' ; format = [1218.90,1729.13]
-
when 'RA3' ; format = [864.57,1218.90]
-
when 'RA4' ; format = [609.45,864.57]
-
when 'SRA0' ; format = [2551.18,3628.35]
-
when 'SRA1' ; format = [1814.17,2551.18]
-
when 'SRA2' ; format = [1275.59,1814.17]
-
when 'SRA3' ; format = [907.09,1275.59]
-
when 'SRA4' ; format = [637.80,907.09]
-
when 'LETTER' ; format = [612.00,792.00]
-
when 'LEGAL' ; format = [612.00,1008.00]
-
when 'EXECUTIVE' ; format = [521.86,756.00]
-
when 'FOLIO' ; format = [612.00,936.00]
-
#else then Error("Unknown page format: #{format}"
-
end
-
@fw_pt = format[0]
-
@fh_pt = format[1]
-
else
-
@fw_pt = format[0]*@k
-
@fh_pt = format[1]*@k
-
end
-
-
@fw = @fw_pt/@k
-
@fh = @fh_pt/@k
-
-
#Page orientation
-
orientation = orientation.downcase
-
if orientation == 'p' or orientation == 'portrait'
-
@def_orientation = 'P'
-
@w_pt = @fw_pt
-
@h_pt = @fh_pt
-
elsif orientation == 'l' or orientation == 'landscape'
-
@def_orientation = 'L'
-
@w_pt = @fh_pt
-
@h_pt = @fw_pt
-
else
-
Error("Incorrect orientation: #{orientation}")
-
end
-
-
@cur_orientation = @def_orientation
-
@w = @w_pt/@k
-
@h = @h_pt/@k
-
#Page margins (1 cm)
-
margin = 28.35/@k
-
SetMargins(margin, margin)
-
#Interior cell margin (1 mm)
-
@c_margin = margin / 10
-
#Line width (0.2 mm)
-
@line_width = 0.567 / @k
-
#Automatic page break
-
SetAutoPageBreak(true, 2 * margin)
-
#Full width display mode
-
SetDisplayMode('fullwidth')
-
#Compression
-
SetCompression(true)
-
#Set default PDF version number
-
@pdf_version = "1.3"
-
-
@encoding = encoding
-
@b = 0
-
@i = 0
-
@u = 0
-
@href = ''
-
@fontlist = ["arial", "times", "courier", "helvetica", "symbol"]
-
@issetfont = false
-
@issetcolor = false
-
-
SetFillColor(200, 200, 200, true)
-
SetTextColor(0, 0, 0, true)
-
end
-
-
#
-
# Set the image scale.
-
# @param float :scale image scale.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def SetImageScale(scale)
-
@img_scale = scale;
-
end
-
1
alias_method :set_image_scale, :SetImageScale
-
-
#
-
# Returns the image scale.
-
# @return float image scale.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetImageScale()
-
return @img_scale;
-
end
-
1
alias_method :get_image_scale, :GetImageScale
-
-
#
-
# Returns the page width in units.
-
# @return int page width.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetPageWidth()
-
return @w;
-
end
-
1
alias_method :get_page_width, :GetPageWidth
-
-
#
-
# Returns the page height in units.
-
# @return int page height.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetPageHeight()
-
return @h;
-
end
-
1
alias_method :get_page_height, :GetPageHeight
-
-
#
-
# Returns the page break margin.
-
# @return int page break margin.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetBreakMargin()
-
return @b_margin;
-
end
-
1
alias_method :get_break_margin, :GetBreakMargin
-
-
#
-
# Returns the scale factor (number of points in user unit).
-
# @return int scale factor.
-
# @author Nicola Asuni
-
# @since 1.5.2
-
#
-
1
def GetScaleFactor()
-
return @k;
-
end
-
1
alias_method :get_scale_factor, :GetScaleFactor
-
-
#
-
# Defines the left, top and right margins. By default, they equal 1 cm. Call this method to change them.
-
# @param float :left Left margin.
-
# @param float :top Top margin.
-
# @param float :right Right margin. Default value is the left one.
-
# @since 1.0
-
# @see SetLeftMargin(), SetTopMargin(), SetRightMargin(), SetAutoPageBreak()
-
#
-
1
def SetMargins(left, top, right=-1)
-
#Set left, top and right margins
-
@l_margin = left
-
@t_margin = top
-
if (right == -1)
-
right = left
-
end
-
@r_margin = right
-
end
-
1
alias_method :set_margins, :SetMargins
-
-
#
-
# Defines the left margin. The method can be called before creating the first page. If the current abscissa gets out of page, it is brought back to the margin.
-
# @param float :margin The margin.
-
# @since 1.4
-
# @see SetTopMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins()
-
#
-
1
def SetLeftMargin(margin)
-
#Set left margin
-
@l_margin = margin
-
if ((@page>0) and (@x < margin))
-
@x = margin
-
end
-
end
-
1
alias_method :set_left_margin, :SetLeftMargin
-
-
#
-
# Defines the top margin. The method can be called before creating the first page.
-
# @param float :margin The margin.
-
# @since 1.5
-
# @see SetLeftMargin(), SetRightMargin(), SetAutoPageBreak(), SetMargins()
-
#
-
1
def SetTopMargin(margin)
-
#Set top margin
-
@t_margin = margin
-
end
-
1
alias_method :set_top_margin, :SetTopMargin
-
-
#
-
# Defines the right margin. The method can be called before creating the first page.
-
# @param float :margin The margin.
-
# @since 1.5
-
# @see SetLeftMargin(), SetTopMargin(), SetAutoPageBreak(), SetMargins()
-
#
-
1
def SetRightMargin(margin)
-
#Set right margin
-
@r_margin = margin
-
end
-
1
alias_method :set_right_margin, :SetRightMargin
-
-
#
-
# Enables or disables the automatic page breaking mode. When enabling, the second parameter is the distance from the bottom of the page that defines the triggering limit. By default, the mode is on and the margin is 2 cm.
-
# @param boolean :auto Boolean indicating if mode should be on or off.
-
# @param float :margin Distance from the bottom of the page.
-
# @since 1.0
-
# @see Cell(), MultiCell(), AcceptPageBreak()
-
#
-
1
def SetAutoPageBreak(auto, margin=0)
-
#Set auto page break mode and triggering margin
-
@auto_page_break = auto
-
@b_margin = margin
-
@page_break_trigger = @h - margin
-
end
-
1
alias_method :set_auto_page_break, :SetAutoPageBreak
-
-
#
-
# Defines the way the document is to be displayed by the viewer. The zoom level can be set: pages can be displayed entirely on screen, occupy the full width of the window, use real size, be scaled by a specific zooming factor or use viewer default (configured in the Preferences menu of Acrobat). The page layout can be specified too: single at once, continuous display, two columns or viewer default. By default, documents use the full width mode with continuous display.
-
# @param mixed :zoom The zoom to use. It can be one of the following string values or a number indicating the zooming factor to use. <ul><li>fullpage: displays the entire page on screen </li><li>fullwidth: uses maximum width of window</li><li>real: uses real size (equivalent to 100% zoom)</li><li>default: uses viewer default mode</li></ul>
-
# @param string :layout The page layout. Possible values are:<ul><li>single: displays one page at once</li><li>continuous: displays pages continuously (default)</li><li>two: displays two pages on two columns</li><li>default: uses viewer default mode</li></ul>
-
# @since 1.2
-
#
-
1
def SetDisplayMode(zoom, layout = 'continuous')
-
#Set display mode in viewer
-
if (zoom == 'fullpage' or zoom == 'fullwidth' or zoom == 'real' or zoom == 'default' or !zoom.is_a?(String))
-
@zoom_mode = zoom
-
else
-
Error("Incorrect zoom display mode: #{zoom}")
-
end
-
if (layout == 'single' or layout == 'continuous' or layout == 'two' or layout == 'default')
-
@layout_mode = layout
-
else
-
Error("Incorrect layout display mode: #{layout}")
-
end
-
end
-
1
alias_method :set_display_mode, :SetDisplayMode
-
-
#
-
# Activates or deactivates page compression. When activated, the internal representation of each page is compressed, which leads to a compression ratio of about 2 for the resulting document. Compression is on by default.
-
# Note: the Zlib extension is required for this feature. If not present, compression will be turned off.
-
# @param boolean :compress Boolean indicating if compression must be enabled.
-
# @since 1.4
-
#
-
1
def SetCompression(compress)
-
#Set page compression
-
if (respond_to?('gzcompress'))
-
@compress = compress
-
else
-
@compress = false
-
end
-
end
-
1
alias_method :set_compression, :SetCompression
-
-
#
-
# Defines the title of the document.
-
# @param string :title The title.
-
# @since 1.2
-
# @see SetAuthor(), SetCreator(), SetKeywords(), SetSubject()
-
#
-
1
def SetTitle(title)
-
#Title of document
-
@title = title
-
end
-
1
alias_method :set_title, :SetTitle
-
-
#
-
# Defines the subject of the document.
-
# @param string :subject The subject.
-
# @since 1.2
-
# @see SetAuthor(), SetCreator(), SetKeywords(), SetTitle()
-
#
-
1
def SetSubject(subject)
-
#Subject of document
-
@subject = subject
-
end
-
1
alias_method :set_subject, :SetSubject
-
-
#
-
# Defines the author of the document.
-
# @param string :author The name of the author.
-
# @since 1.2
-
# @see SetCreator(), SetKeywords(), SetSubject(), SetTitle()
-
#
-
1
def SetAuthor(author)
-
#Author of document
-
@author = author
-
end
-
1
alias_method :set_author, :SetAuthor
-
-
#
-
# Associates keywords with the document, generally in the form 'keyword1 keyword2 ...'.
-
# @param string :keywords The list of keywords.
-
# @since 1.2
-
# @see SetAuthor(), SetCreator(), SetSubject(), SetTitle()
-
#
-
1
def SetKeywords(keywords)
-
#Keywords of document
-
@keywords = keywords
-
end
-
1
alias_method :set_keywords, :SetKeywords
-
-
#
-
# Defines the creator of the document. This is typically the name of the application that generates the PDF.
-
# @param string :creator The name of the creator.
-
# @since 1.2
-
# @see SetAuthor(), SetKeywords(), SetSubject(), SetTitle()
-
#
-
1
def SetCreator(creator)
-
#Creator of document
-
@creator = creator
-
end
-
1
alias_method :set_creator, :SetCreator
-
-
#
-
# Defines an alias for the total number of pages. It will be substituted as the document is closed.<br />
-
# <b>Example:</b><br />
-
# <pre>
-
# class PDF extends TCPDF {
-
# def Footer()
-
# #Go to 1.5 cm from bottom
-
# SetY(-15);
-
# #Select Arial italic 8
-
# SetFont('Arial','I',8);
-
# #Print current and total page numbers
-
# Cell(0,10,'Page '.PageNo().'/{nb}',0,0,'C');
-
# end
-
# }
-
# :pdf=new PDF();
-
# :pdf->alias_nb_pages();
-
# </pre>
-
# @param string :alias The alias. Default valuenb}.
-
# @since 1.4
-
# @see PageNo(), Footer()
-
#
-
1
def AliasNbPages(alias_nb ='{nb}')
-
#Define an alias for total number of pages
-
@alias_nb_pages = escapetext(alias_nb)
-
end
-
1
alias_method :alias_nb_pages, :AliasNbPages
-
-
#
-
# This method is automatically called in case of fatal error; it simply outputs the message and halts the execution. An inherited class may override it to customize the error handling but should always halt the script, or the resulting document would probably be invalid.
-
# 2004-06-11 :: Nicola Asuni : changed bold tag with strong
-
# @param string :msg The error message
-
# @since 1.0
-
#
-
1
def Error(msg)
-
#Fatal error
-
raise ("TCPDF error: #{msg}")
-
end
-
1
alias_method :error, :Error
-
-
#
-
# This method begins the generation of the PDF document. It is not necessary to call it explicitly because AddPage() does it automatically.
-
# Note: no page is created by this method
-
# @since 1.0
-
# @see AddPage(), Close()
-
#
-
1
def Open()
-
#Begin document
-
@state = 1
-
end
-
# alias_method :open, :Open
-
-
#
-
# Terminates the PDF document. It is not necessary to call this method explicitly because Output() does it automatically. If the document contains no page, AddPage() is called to prevent from getting an invalid document.
-
# @since 1.0
-
# @see Open(), Output()
-
#
-
1
def Close()
-
#Terminate document
-
if (@state==3)
-
return;
-
end
-
if (@page==0)
-
AddPage();
-
end
-
#Page footer
-
@in_footer=true;
-
Footer();
-
@in_footer=false;
-
#Close page
-
endpage();
-
#Close document
-
enddoc();
-
end
-
# alias_method :close, :Close
-
-
#
-
# Adds a new page to the document. If a page is already present, the Footer() method is called first to output the footer. Then the page is added, the current position set to the top-left corner according to the left and top margins, and Header() is called to display the header.
-
# The font which was set before calling is automatically restored. There is no need to call SetFont() again if you want to continue with the same font. The same is true for colors and line width.
-
# The origin of the coordinate system is at the top-left corner and increasing ordinates go downwards.
-
# @param string :orientation Page orientation. Possible values are (case insensitive):<ul><li>P or Portrait</li><li>L or Landscape</li></ul> The default value is the one passed to the constructor.
-
# @since 1.0
-
# @see TCPDF(), Header(), Footer(), SetMargins()
-
#
-
1
def AddPage(orientation='')
-
#Start a new page
-
if (@state==0)
-
Open();
-
end
-
family=@font_family;
-
style=@font_style + (@underline ? 'U' : '') + (@deleted ? 'D' : '');
-
size=@font_size_pt;
-
lw=@line_width;
-
dc=@draw_color;
-
fc=@fill_color;
-
tc=@text_color;
-
cf=@color_flag;
-
if (@page>0)
-
#Page footer
-
@in_footer=true;
-
Footer();
-
@in_footer=false;
-
#Close page
-
endpage();
-
end
-
#Start new page
-
beginpage(orientation);
-
#Set line cap style to square
-
out('2 J');
-
#Set line width
-
@line_width = lw;
-
out(sprintf('%.2f w', lw*@k));
-
#Set font
-
if (family)
-
SetFont(family, style, size);
-
end
-
#Set colors
-
@draw_color = dc;
-
if (dc!='0 G')
-
out(dc);
-
end
-
@fill_color = fc;
-
if (fc!='0 g')
-
out(fc);
-
end
-
@text_color = tc;
-
@color_flag = cf;
-
#Page header
-
Header();
-
#Restore line width
-
if (@line_width != lw)
-
@line_width = lw;
-
out(sprintf('%.2f w', lw*@k));
-
end
-
#Restore font
-
if (family)
-
SetFont(family, style, size);
-
end
-
#Restore colors
-
if (@draw_color != dc)
-
@draw_color = dc;
-
out(dc);
-
end
-
if (@fill_color != fc)
-
@fill_color = fc;
-
out(fc);
-
end
-
@text_color = tc;
-
@color_flag = cf;
-
end
-
1
alias_method :add_page, :AddPage
-
-
#
-
# Rotate object.
-
# @param float :angle angle in degrees for counter-clockwise rotation
-
# @param int :x abscissa of the rotation center. Default is current x position
-
# @param int :y ordinate of the rotation center. Default is current y position
-
#
-
1
def Rotate(angle, x="", y="")
-
-
if (x == '')
-
x = @x;
-
end
-
-
if (y == '')
-
y = @y;
-
end
-
-
if (@rtl)
-
x = @w - x;
-
angle = -@angle;
-
end
-
-
y = (@h - y) * @k;
-
x *= @k;
-
-
# calculate elements of transformation matrix
-
tm = []
-
tm[0] = ::Math::cos(deg2rad(angle));
-
tm[1] = ::Math::sin(deg2rad(angle));
-
tm[2] = -tm[1];
-
tm[3] = tm[0];
-
tm[4] = x + tm[1] * y - tm[0] * x;
-
tm[5] = y - tm[0] * y - tm[1] * x;
-
-
# generate the transformation matrix
-
Transform(tm);
-
end
-
1
alias_method :rotate, :Rotate
-
-
#
-
# Starts a 2D tranformation saving current graphic state.
-
# This function must be called before scaling, mirroring, translation, rotation and skewing.
-
# Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior.
-
#
-
1
def StartTransform
-
out('q');
-
end
-
1
alias_method :start_transform, :StartTransform
-
-
#
-
# Stops a 2D tranformation restoring previous graphic state.
-
# This function must be called after scaling, mirroring, translation, rotation and skewing.
-
# Use StartTransform() before, and StopTransform() after the transformations to restore the normal behavior.
-
#
-
1
def StopTransform
-
out('Q');
-
end
-
1
alias_method :stop_transform, :StopTransform
-
-
#
-
# Apply graphic transformations.
-
# @since 2.1.000 (2008-01-07)
-
# @see StartTransform(), StopTransform()
-
#
-
1
def Transform(tm)
-
x = out(sprintf('%.3f %.3f %.3f %.3f %.3f %.3f cm', tm[0], tm[1], tm[2], tm[3], tm[4], tm[5]));
-
end
-
1
alias_method :transform, :Transform
-
-
#
-
# Set header data.
-
# @param string :ln header image logo
-
# @param string :lw header image logo width in mm
-
# @param string :ht string to print as title on document header
-
# @param string :hs string to print on document header
-
#
-
1
def SetHeaderData(ln="", lw=0, ht="", hs="")
-
@header_logo = ln || ""
-
@header_logo_width = lw || 0
-
@header_title = ht || ""
-
@header_string = hs || ""
-
end
-
1
alias_method :set_header_data, :SetHeaderData
-
-
#
-
# Set header margin.
-
# (minimum distance between header and top page margin)
-
# @param int :hm distance in millimeters
-
#
-
1
def SetHeaderMargin(hm=10)
-
@header_margin = hm;
-
end
-
1
alias_method :set_header_margin, :SetHeaderMargin
-
-
#
-
# Set footer margin.
-
# (minimum distance between footer and bottom page margin)
-
# @param int :fm distance in millimeters
-
#
-
1
def SetFooterMargin(fm=10)
-
@footer_margin = fm;
-
end
-
1
alias_method :set_footer_margin, :SetFooterMargin
-
-
#
-
# Set a flag to print page header.
-
# @param boolean :val set to true to print the page header (default), false otherwise.
-
#
-
1
def SetPrintHeader(val=true)
-
@print_header = val;
-
end
-
1
alias_method :set_print_header, :SetPrintHeader
-
-
#
-
# Set a flag to print page footer.
-
# @param boolean :value set to true to print the page footer (default), false otherwise.
-
#
-
1
def SetPrintFooter(val=true)
-
@print_footer = val;
-
end
-
1
alias_method :set_print_footer, :SetPrintFooter
-
-
#
-
# This method is used to render the page header.
-
# It is automatically called by AddPage() and could be overwritten in your own inherited class.
-
#
-
1
def Header()
-
if (@print_header)
-
if (@original_l_margin.nil?)
-
@original_l_margin = @l_margin;
-
end
-
if (@original_r_margin.nil?)
-
@original_r_margin = @r_margin;
-
end
-
-
#set current position
-
SetXY(@original_l_margin, @header_margin);
-
-
if ((@header_logo) and (@header_logo != @@k_blank_image))
-
Image(@header_logo, @original_l_margin, @header_margin, @header_logo_width);
-
else
-
@img_rb_y = GetY();
-
end
-
-
cell_height = ((@@k_cell_height_ratio * @header_font[2]) / @k).round(2)
-
-
header_x = @original_l_margin + (@header_logo_width * 1.05); #set left margin for text data cell
-
-
# header title
-
SetFont(@header_font[0], 'B', @header_font[2] + 1);
-
SetX(header_x);
-
Cell(@header_width, cell_height, @header_title, 0, 1, 'L');
-
-
# header string
-
SetFont(@header_font[0], @header_font[1], @header_font[2]);
-
SetX(header_x);
-
MultiCell(@header_width, cell_height, @header_string, 0, 'L', 0);
-
-
# print an ending header line
-
if (@header_width)
-
#set style for cell border
-
SetLineWidth(0.3);
-
SetDrawColor(0, 0, 0);
-
SetY(1 + (@img_rb_y > GetY() ? @img_rb_y : GetY()));
-
SetX(@original_l_margin);
-
Cell(0, 0, '', 'T', 0, 'C');
-
end
-
-
#restore position
-
SetXY(@original_l_margin, @t_margin);
-
end
-
end
-
1
alias_method :header, :Header
-
-
#
-
# This method is used to render the page footer.
-
# It is automatically called by AddPage() and could be overwritten in your own inherited class.
-
#
-
1
def Footer()
-
if (@print_footer)
-
-
if (@original_l_margin.nil?)
-
@original_l_margin = @l_margin;
-
end
-
if (@original_r_margin.nil?)
-
@original_r_margin = @r_margin;
-
end
-
-
#set font
-
SetFont(@footer_font[0], @footer_font[1] , @footer_font[2]);
-
#set style for cell border
-
line_width = 0.3;
-
SetLineWidth(line_width);
-
SetDrawColor(0, 0, 0);
-
-
footer_height = ((@@k_cell_height_ratio * @footer_font[2]) / @k).round; #footer height, was , 2)
-
#get footer y position
-
footer_y = @h - @footer_margin - footer_height;
-
#set current position
-
SetXY(@original_l_margin, footer_y);
-
-
#print document barcode
-
if (@barcode)
-
Ln();
-
barcode_width = ((@w - @original_l_margin - @original_r_margin)).round; #max width
-
writeBarcode(@original_l_margin, footer_y + line_width, barcode_width, footer_height - line_width, "C128B", false, false, 2, @barcode);
-
end
-
-
SetXY(@original_l_margin, footer_y);
-
-
#Print page number
-
Cell(0, footer_height, @l['w_page'] + " " + PageNo().to_s + ' / {nb}', 'T', 0, 'R');
-
end
-
end
-
1
alias_method :footer, :Footer
-
-
#
-
# Returns the current page number.
-
# @return int page number
-
# @since 1.0
-
# @see alias_nb_pages()
-
#
-
1
def PageNo()
-
#Get current page number
-
return @page;
-
end
-
1
alias_method :page_no, :PageNo
-
-
#
-
# Defines the color used for all drawing operations (lines, rectangles and cell borders). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page.
-
# @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255
-
# @param int :g Green component (between 0 and 255)
-
# @param int :b Blue component (between 0 and 255)
-
# @since 1.3
-
# @see SetFillColor(), SetTextColor(), Line(), Rect(), Cell(), MultiCell()
-
#
-
1
def SetDrawColor(r, g=-1, b=-1)
-
#Set color for all stroking operations
-
if ((r==0 and g==0 and b==0) or g==-1)
-
@draw_color=sprintf('%.3f G', r/255.0);
-
else
-
@draw_color=sprintf('%.3f %.3f %.3f RG', r/255.0, g/255.0, b/255.0);
-
end
-
if (@page>0)
-
out(@draw_color);
-
end
-
end
-
1
alias_method :set_draw_color, :SetDrawColor
-
-
#
-
# Defines the color used for all filling operations (filled rectangles and cell backgrounds). It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page.
-
# @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255
-
# @param int :g Green component (between 0 and 255)
-
# @param int :b Blue component (between 0 and 255)
-
# @param boolean :storeprev if true stores the RGB array on :prevfill_color variable.
-
# @since 1.3
-
# @see SetDrawColor(), SetTextColor(), Rect(), Cell(), MultiCell()
-
#
-
1
def SetFillColor(r, g=-1, b=-1, storeprev=false)
-
#Set color for all filling operations
-
if ((r==0 and g==0 and b==0) or g==-1)
-
@fill_color=sprintf('%.3f g', r/255.0);
-
else
-
@fill_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0);
-
end
-
@color_flag=(@fill_color!=@text_color);
-
if (@page>0)
-
out(@fill_color);
-
end
-
if (storeprev)
-
# store color as previous value
-
@prevfill_color = [r, g, b]
-
end
-
end
-
1
alias_method :set_fill_color, :SetFillColor
-
-
# This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors
-
1
def SetCmykFillColor(c, m, y, k, storeprev=false)
-
#Set color for all filling operations
-
@fill_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k);
-
@color_flag=(@fill_color!=@text_color);
-
if (storeprev)
-
# store color as previous value
-
@prevtext_color = [c, m, y, k]
-
end
-
if (@page>0)
-
out(@fill_color);
-
end
-
end
-
1
alias_method :set_cmyk_fill_color, :SetCmykFillColor
-
-
#
-
# Defines the color used for text. It can be expressed in RGB components or gray scale. The method can be called before the first page is created and the value is retained from page to page.
-
# @param int :r If g et b are given, red component; if not, indicates the gray level. Value between 0 and 255
-
# @param int :g Green component (between 0 and 255)
-
# @param int :b Blue component (between 0 and 255)
-
# @param boolean :storeprev if true stores the RGB array on :prevtext_color variable.
-
# @since 1.3
-
# @see SetDrawColor(), SetFillColor(), Text(), Cell(), MultiCell()
-
#
-
1
def SetTextColor(r, g=-1, b=-1, storeprev=false)
-
#Set color for text
-
if ((r==0 and :g==0 and :b==0) or :g==-1)
-
@text_color=sprintf('%.3f g', r/255.0);
-
else
-
@text_color=sprintf('%.3f %.3f %.3f rg', r/255.0, g/255.0, b/255.0);
-
end
-
@color_flag=(@fill_color!=@text_color);
-
if (storeprev)
-
# store color as previous value
-
@prevtext_color = [r, g, b]
-
end
-
end
-
1
alias_method :set_text_color, :SetTextColor
-
-
# This hasn't been ported from tcpdf, it's a variation on SetTextColor for setting cmyk colors
-
1
def SetCmykTextColor(c, m, y, k, storeprev=false)
-
#Set color for text
-
@text_color=sprintf('%.3f %.3f %.3f %.3f k', c, m, y, k);
-
@color_flag=(@fill_color!=@text_color);
-
if (storeprev)
-
# store color as previous value
-
@prevtext_color = [c, m, y, k]
-
end
-
end
-
1
alias_method :set_cmyk_text_color, :SetCmykTextColor
-
-
#
-
# Returns the length of a string in user unit. A font must be selected.<br>
-
# Support UTF-8 Unicode [Nicola Asuni, 2005-01-02]
-
# @param string :s The string whose length is to be computed
-
# @return int
-
# @since 1.2
-
#
-
1
def GetStringWidth(s)
-
#Get width of a string in the current font
-
s = s.to_s;
-
cw = @current_font['cw']
-
w = 0;
-
if (@is_unicode)
-
unicode = UTF8StringToArray(s);
-
unicode.each do |char|
-
if (!cw[char].nil?)
-
w += cw[char];
-
# This should not happen. UTF8StringToArray should guarentee the array is ascii values.
-
# elsif (c!cw[char[0]].nil?)
-
# w += cw[char[0]];
-
# elsif (!cw[char.chr].nil?)
-
# w += cw[char.chr];
-
elsif (!@current_font['desc']['MissingWidth'].nil?)
-
w += @current_font['desc']['MissingWidth']; # set default size
-
else
-
w += 500;
-
end
-
end
-
else
-
s.each_byte do |c|
-
if cw[c.chr]
-
w += cw[c.chr];
-
elsif cw[?c.chr]
-
w += cw[?c.chr]
-
end
-
end
-
end
-
return (w * @font_size / 1000.0);
-
end
-
1
alias_method :get_string_width, :GetStringWidth
-
-
#
-
# Defines the line width. By default, the value equals 0.2 mm. The method can be called before the first page is created and the value is retained from page to page.
-
# @param float :width The width.
-
# @since 1.0
-
# @see Line(), Rect(), Cell(), MultiCell()
-
#
-
1
def SetLineWidth(width)
-
#Set line width
-
@line_width = width;
-
if (@page>0)
-
out(sprintf('%.2f w', width*@k));
-
end
-
end
-
1
alias_method :set_line_width, :SetLineWidth
-
-
#
-
# Draws a line between two points.
-
# @param float :x1 Abscissa of first point
-
# @param float :y1 Ordinate of first point
-
# @param float :x2 Abscissa of second point
-
# @param float :y2 Ordinate of second point
-
# @since 1.0
-
# @see SetLineWidth(), SetDrawColor()
-
#
-
1
def Line(x1, y1, x2, y2)
-
#Draw a line
-
out(sprintf('%.2f %.2f m %.2f %.2f l S', x1 * @k, (@h - y1) * @k, x2 * @k, (@h - y2) * @k));
-
end
-
1
alias_method :line, :Line
-
-
1
def Circle(mid_x, mid_y, radius, style='')
-
mid_y = (@h-mid_y)*@k
-
out(sprintf("q\n")) # postscript content in pdf
-
# init line type etc. with /GSD gs G g (grey) RG rg (RGB) w=line witdh etc.
-
out(sprintf("1 j\n")) # line join
-
# translate ("move") circle to mid_y, mid_y
-
out(sprintf("1 0 0 1 %f %f cm", mid_x, mid_y))
-
kappa = 0.5522847498307933984022516322796
-
# Quadrant 1
-
x_s = 0.0 # 12 o'clock
-
y_s = 0.0 + radius
-
x_e = 0.0 + radius # 3 o'clock
-
y_e = 0.0
-
out(sprintf("%f %f m\n", x_s, y_s)) # move to 12 o'clock
-
# cubic bezier control point 1, start height and kappa * radius to the right
-
bx_e1 = x_s + (radius * kappa)
-
by_e1 = y_s
-
# cubic bezier control point 2, end and kappa * radius above
-
bx_e2 = x_e
-
by_e2 = y_e + (radius * kappa)
-
# draw cubic bezier from current point to x_e/y_e with bx_e1/by_e1 and bx_e2/by_e2 as bezier control points
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
# Quadrant 2
-
x_s = x_e
-
y_s = y_e # 3 o'clock
-
x_e = 0.0
-
y_e = 0.0 - radius # 6 o'clock
-
bx_e1 = x_s # cubic bezier point 1
-
by_e1 = y_s - (radius * kappa)
-
bx_e2 = x_e + (radius * kappa) # cubic bezier point 2
-
by_e2 = y_e
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
# Quadrant 3
-
x_s = x_e
-
y_s = y_e # 6 o'clock
-
x_e = 0.0 - radius
-
y_e = 0.0 # 9 o'clock
-
bx_e1 = x_s - (radius * kappa) # cubic bezier point 1
-
by_e1 = y_s
-
bx_e2 = x_e # cubic bezier point 2
-
by_e2 = y_e - (radius * kappa)
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
# Quadrant 4
-
x_s = x_e
-
y_s = y_e # 9 o'clock
-
x_e = 0.0
-
y_e = 0.0 + radius # 12 o'clock
-
bx_e1 = x_s # cubic bezier point 1
-
by_e1 = y_s + (radius * kappa)
-
bx_e2 = x_e - (radius * kappa) # cubic bezier point 2
-
by_e2 = y_e
-
out(sprintf("%f %f %f %f %f %f c\n", bx_e1, by_e1, bx_e2, by_e2, x_e, y_e))
-
if style=='F'
-
op='f'
-
elsif style=='FD' or style=='DF'
-
op='b'
-
else
-
op='s'
-
end
-
out(sprintf("#{op}\n")) # stroke circle, do not fill and close path
-
# for filling etc. b, b*, f, f*
-
out(sprintf("Q\n")) # finish postscript in PDF
-
end
-
1
alias_method :circle, :Circle
-
-
#
-
# Outputs a rectangle. It can be drawn (border only), filled (with no border) or both.
-
# @param float :x Abscissa of upper-left corner
-
# @param float :y Ordinate of upper-left corner
-
# @param float :w Width
-
# @param float :h Height
-
# @param string :style Style of rendering. Possible values are:<ul><li>D or empty string: draw (default)</li><li>F: fill</li><li>DF or FD: draw and fill</li></ul>
-
# @since 1.0
-
# @see SetLineWidth(), SetDrawColor(), SetFillColor()
-
#
-
1
def Rect(x, y, w, h, style='')
-
#Draw a rectangle
-
if (style=='F')
-
op='f';
-
elsif (style=='FD' or style=='DF')
-
op='B';
-
else
-
op='S';
-
end
-
out(sprintf('%.2f %.2f %.2f %.2f re %s', x * @k, (@h - y) * @k, w * @k, -h * @k, op));
-
end
-
1
alias_method :rect, :Rect
-
-
#
-
# Imports a TrueType or Type1 font and makes it available. It is necessary to generate a font definition file first with the makefont.rb utility. The definition file (and the font file itself when embedding) must be present either in the current directory or in the one indicated by FPDF_FONTPATH if the constant is defined. If it could not be found, the error "Could not include font definition file" is generated.
-
# Support UTF-8 Unicode [Nicola Asuni, 2005-01-02].
-
# <b>Example</b>:<br />
-
# <pre>
-
# :pdf->AddFont('Comic','I');
-
# # is equivalent to:
-
# :pdf->AddFont('Comic','I','comici.rb');
-
# </pre>
-
# @param string :family Font family. The name can be chosen arbitrarily. If it is a standard family name, it will override the corresponding font.
-
# @param string :style Font style. Possible values are (case insensitive):<ul><li>empty string: regular (default)</li><li>B: bold</li><li>I: italic</li><li>BI or IB: bold italic</li></ul>
-
# @param string :file The font definition file. By default, the name is built from the family and style, in lower case with no space.
-
# @since 1.5
-
# @see SetFont()
-
#
-
1
def AddFont(family, style='', file='')
-
if (family.empty?)
-
return;
-
end
-
-
#Add a TrueType or Type1 font
-
family = family.downcase
-
if ((!@is_unicode) and (family == 'arial'))
-
family = 'helvetica';
-
end
-
-
style=style.upcase
-
style=style.gsub('U','');
-
style=style.gsub('D','');
-
if (style == 'IB')
-
style = 'BI';
-
end
-
-
fontkey = family + style;
-
# check if the font has been already added
-
if !@fonts[fontkey].nil?
-
return;
-
end
-
-
if (file=='')
-
file = family.gsub(' ', '') + style.downcase + '.rb';
-
end
-
font_file_name = getfontpath(file)
-
if (font_file_name.nil?)
-
# try to load the basic file without styles
-
file = family.gsub(' ', '') + '.rb';
-
font_file_name = getfontpath(file)
-
end
-
if font_file_name.nil?
-
Error("Could not find font #{file}.")
-
end
-
require(getfontpath(file))
-
font_desc = TCPDFFontDescriptor.font(file)
-
-
if (font_desc[:name].nil? and @@fpdf_charwidths.nil?)
-
Error('Could not include font definition file');
-
end
-
-
i = @fonts.length+1;
-
if (@is_unicode)
-
@fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg], 'cMap' => font_desc[:cMap], 'registry' => font_desc[:registry]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
else
-
@fonts[fontkey]={'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
end
-
-
if (!font_desc[:diff].nil? and (!font_desc[:diff].empty?))
-
#Search existing encodings
-
d=0;
-
nb=@diffs.length;
-
1.upto(nb) do |i|
-
if (@diffs[i]== font_desc[:diff])
-
d = i;
-
break;
-
end
-
end
-
if (d==0)
-
d = nb+1;
-
@diffs[d] = font_desc[:diff];
-
end
-
@fonts[fontkey]['diff'] = d;
-
end
-
if (font_desc[:file] and font_desc[:file].length > 0)
-
if (font_desc[:type] == "TrueType") or (font_desc[:type] == "TrueTypeUnicode")
-
@font_files[font_desc[:file]] = {'length1' => font_desc[:originalsize]}
-
else
-
@font_files[font_desc[:file]] = {'length1' => font_desc[:size1], 'length2' => font_desc[:size2]}
-
end
-
end
-
end
-
1
alias_method :add_font, :AddFont
-
-
#
-
# Sets the font used to print character strings. It is mandatory to call this method at least once before printing text or the resulting document would not be valid.
-
# The font can be either a standard one or a font added via the AddFont() method. Standard fonts use Windows encoding cp1252 (Western Europe).
-
# The method can be called before the first page is created and the font is retained from page to page.
-
# If you just wish to change the current font size, it is simpler to call SetFontSize().
-
# Note: for the standard fonts, the font metric files must be accessible. There are three possibilities for this:<ul><li>They are in the current directory (the one where the running script lies)</li><li>They are in one of the directories defined by the include_path parameter</li><li>They are in the directory defined by the FPDF_FONTPATH constant</li></ul><br />
-
# Example for the last case (note the trailing slash):<br />
-
# <pre>
-
# define('FPDF_FONTPATH','/home/www/font/');
-
# require('tcpdf.rb');
-
#
-
# #Times regular 12
-
# :pdf->SetFont('Times');
-
# #Arial bold 14
-
# :pdf->SetFont('Arial','B',14);
-
# #Removes bold
-
# :pdf->SetFont('');
-
# #Times bold, italic and underlined 14
-
# :pdf->SetFont('Times','BIUD');
-
# </pre><br />
-
# If the file corresponding to the requested font is not found, the error "Could not include font metric file" is generated.
-
# @param string :family Family font. It can be either a name defined by AddFont() or one of the standard families (case insensitive):<ul><li>Courier (fixed-width)</li><li>Helvetica or Arial (synonymous; sans serif)</li><li>Times (serif)</li><li>Symbol (symbolic)</li><li>ZapfDingbats (symbolic)</li></ul>It is also possible to pass an empty string. In that case, the current family is retained.
-
# @param string :style Font style. Possible values are (case insensitive):<ul><li>empty string: regular</li><li>B: bold</li><li>I: italic</li><li>U: underline</li></ul>or any combination. The default value is regular. Bold and italic styles do not apply to Symbol and ZapfDingbats
-
# @param float :size Font size in points. The default value is the current size. If no size has been specified since the beginning of the document, the value taken is 12
-
# @since 1.0
-
# @see AddFont(), SetFontSize(), Cell(), MultiCell(), Write()
-
#
-
1
def SetFont(family, style='', size=0)
-
# save previous values
-
@prevfont_family = @font_family;
-
@prevfont_style = @font_style;
-
-
family=family.downcase;
-
if (family=='')
-
family=@font_family;
-
end
-
if ((!@is_unicode) and (family == 'arial'))
-
family = 'helvetica';
-
elsif ((family=="symbol") or (family=="zapfdingbats"))
-
style='';
-
end
-
-
style=style.upcase;
-
-
if (style.include?('U'))
-
@underline=true;
-
style= style.gsub('U','');
-
else
-
@underline=false;
-
end
-
if (style.include?('D'))
-
@deleted=true;
-
style= style.gsub('D','');
-
else
-
@deleted=false;
-
end
-
if (style=='IB')
-
style='BI';
-
end
-
if (size==0)
-
size=@font_size_pt;
-
end
-
-
# try to add font (if not already added)
-
AddFont(family, style);
-
-
#Test if font is already selected
-
if ((@font_family == family) and (@font_style == style) and (@font_size_pt == size))
-
return;
-
end
-
-
fontkey = family + style;
-
style = '' if (@fonts[fontkey].nil? and !@fonts[family].nil?)
-
-
#Test if used for the first time
-
if (@fonts[fontkey].nil?)
-
#Check if one of the standard fonts
-
if (!@core_fonts[fontkey].nil?)
-
if @@fpdf_charwidths[fontkey].nil?
-
#Load metric file
-
file = family;
-
if ((family!='symbol') and (family!='zapfdingbats'))
-
file += style.downcase;
-
end
-
if (getfontpath(file + '.rb').nil?)
-
# try to load the basic file without styles
-
file = family;
-
fontkey = family;
-
end
-
require(getfontpath(file + '.rb'));
-
font_desc = TCPDFFontDescriptor.font(file)
-
if ((@is_unicode and ctg.nil?) or ((!@is_unicode) and (@@fpdf_charwidths[fontkey].nil?)) )
-
Error("Could not include font metric file [" + fontkey + "]: " + getfontpath(file + ".rb"));
-
end
-
end
-
i = @fonts.length + 1;
-
-
if (@is_unicode)
-
@fonts[fontkey] = {'i' => i, 'type' => font_desc[:type], 'name' => font_desc[:name], 'desc' => font_desc[:desc], 'up' => font_desc[:up], 'ut' => font_desc[:ut], 'cw' => font_desc[:cw], 'enc' => font_desc[:enc], 'file' => font_desc[:file], 'ctg' => font_desc[:ctg]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
else
-
@fonts[fontkey] = {'i' => i, 'type'=>'core', 'name'=>@core_fonts[fontkey], 'up'=>-100, 'ut'=>50, 'cw' => font_desc[:cw]}
-
@@fpdf_charwidths[fontkey] = font_desc[:cw];
-
end
-
else
-
Error('Undefined font: ' + family + ' ' + style);
-
end
-
end
-
#Select it
-
@font_family = family;
-
@font_style = style;
-
@font_size_pt = size;
-
@font_size = size / @k;
-
@current_font = @fonts[fontkey]; # was & may need deep copy?
-
if (@page>0)
-
out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt));
-
end
-
end
-
1
alias_method :set_font, :SetFont
-
-
#
-
# Defines the size of the current font.
-
# @param float :size The size (in points)
-
# @since 1.0
-
# @see SetFont()
-
#
-
1
def SetFontSize(size)
-
#Set font size in points
-
if (@font_size_pt== size)
-
return;
-
end
-
@font_size_pt = size;
-
@font_size = size.to_f / @k;
-
if (@page > 0)
-
out(sprintf('BT /F%d %.2f Tf ET', @current_font['i'], @font_size_pt));
-
end
-
end
-
1
alias_method :set_font_size, :SetFontSize
-
-
#
-
# Creates a new internal link and returns its identifier. An internal link is a clickable area which directs to another place within the document.<br />
-
# The identifier can then be passed to Cell(), Write(), Image() or Link(). The destination is defined with SetLink().
-
# @since 1.5
-
# @see Cell(), Write(), Image(), Link(), SetLink()
-
#
-
1
def AddLink()
-
#Create a new internal link
-
n=@links.length+1;
-
@links[n]=[0,0];
-
return n;
-
end
-
1
alias_method :add_link, :AddLink
-
-
#
-
# Defines the page and position a link points to
-
# @param int :link The link identifier returned by AddLink()
-
# @param float :y Ordinate of target position; -1 indicates the current position. The default value is 0 (top of page)
-
# @param int :page Number of target page; -1 indicates the current page. This is the default value
-
# @since 1.5
-
# @see AddLink()
-
#
-
1
def SetLink(link, y=0, page=-1)
-
#Set destination of internal link
-
if (y==-1)
-
y=@y;
-
end
-
if (page==-1)
-
page=@page;
-
end
-
@links[link] = [page, y]
-
end
-
1
alias_method :set_link, :SetLink
-
-
#
-
# Puts a link on a rectangular area of the page. Text or image links are generally put via Cell(), Write() or Image(), but this method can be useful for instance to define a clickable area inside an image.
-
# @param float :x Abscissa of the upper-left corner of the rectangle
-
# @param float :y Ordinate of the upper-left corner of the rectangle
-
# @param float :w Width of the rectangle
-
# @param float :h Height of the rectangle
-
# @param mixed :link URL or identifier returned by AddLink()
-
# @since 1.5
-
# @see AddLink(), Cell(), Write(), Image()
-
#
-
1
def Link(x, y, w, h, link)
-
#Put a link on the page
-
@page_links ||= Array.new
-
@page_links[@page] ||= Array.new
-
@page_links[@page].push([x * @k, @h_pt - y * @k, w * @k, h*@k, link]);
-
end
-
1
alias_method :link, :Link
-
-
#
-
# Prints a character string. The origin is on the left of the first charcter, on the baseline. This method allows to place a string precisely on the page, but it is usually easier to use Cell(), MultiCell() or Write() which are the standard methods to print text.
-
# @param float :x Abscissa of the origin
-
# @param float :y Ordinate of the origin
-
# @param string :txt String to print
-
# @since 1.0
-
# @see SetFont(), SetTextColor(), Cell(), MultiCell(), Write()
-
#
-
1
def Text(x, y, txt)
-
#Output a string
-
s=sprintf('BT %.2f %.2f Td (%s) Tj ET', x * @k, (@h-y) * @k, escapetext(txt));
-
if (@underline and (txt!=''))
-
s += ' ' + dolinetxt(x, y, txt);
-
end
-
if (@color_flag)
-
s='q ' + @text_color + ' ' + s + ' Q';
-
end
-
out(s);
-
end
-
1
alias_method :text, :Text
-
-
#
-
# Whenever a page break condition is met, the method is called, and the break is issued or not depending on the returned value. The default implementation returns a value according to the mode selected by SetAutoPageBreak().<br />
-
# This method is called automatically and should not be called directly by the application.<br />
-
# <b>Example:</b><br />
-
# The method is overriden in an inherited class in order to obtain a 3 column layout:<br />
-
# <pre>
-
# class PDF extends TCPDF {
-
# var :col=0;
-
#
-
# def SetCol(col)
-
# #Move position to a column
-
# @col = col;
-
# :x=10+:col*65;
-
# SetLeftMargin(x);
-
# SetX(x);
-
# end
-
#
-
# def AcceptPageBreak()
-
# if (@col<2)
-
# #Go to next column
-
# SetCol(@col+1);
-
# SetY(10);
-
# return false;
-
# end
-
# else
-
# #Go back to first column and issue page break
-
# SetCol(0);
-
# return true;
-
# end
-
# end
-
# }
-
#
-
# :pdf=new PDF();
-
# :pdf->Open();
-
# :pdf->AddPage();
-
# :pdf->SetFont('Arial','',12);
-
# for(i=1;:i<=300;:i++)
-
# :pdf->Cell(0,5,"Line :i",0,1);
-
# }
-
# :pdf->Output();
-
# </pre>
-
# @return boolean
-
# @since 1.4
-
# @see SetAutoPageBreak()
-
#
-
1
def AcceptPageBreak()
-
#Accept automatic page break or not
-
return @auto_page_break;
-
end
-
1
alias_method :accept_page_break, :AcceptPageBreak
-
-
1
def BreakThePage?(h)
-
if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
true
-
else
-
false
-
end
-
end
-
1
alias_method :break_the_page?, :BreakThePage?
-
#
-
# Prints a cell (rectangular area) with optional borders, background color and character string. The upper-left corner of the cell corresponds to the current position. The text can be aligned or centered. After the call, the current position moves to the right or to the next line. It is possible to put a link on the text.<br />
-
# If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting.
-
# @param float :w Cell width. If 0, the cell extends up to the right margin.
-
# @param float :h Cell height. Default value: 0.
-
# @param string :txt String to print. Default value: empty string.
-
# @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:<ul><li>0: no border (default)</li><li>1: frame</li></ul>or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
-
# @param int :ln Indicates where the current position should go after the call. Possible values are:<ul><li>0: to the right</li><li>1: to the beginning of the next line</li><li>2: below</li></ul>
-
# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0.
-
# @param string :align Allows to center or align the text. Possible values are:<ul><li>L or empty string: left align (default value)</li><li>C: center</li><li>R: right align</li></ul>
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @param mixed :link URL or identifier returned by AddLink().
-
# @since 1.0
-
# @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), AddLink(), Ln(), MultiCell(), Write(), SetAutoPageBreak()
-
#
-
1
def Cell(w, h=0, txt='', border=0, ln=0, align='', fill=0, link=nil)
-
#Output a cell
-
k=@k;
-
if ((@y + h) > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
#Automatic page break
-
if @pages[@page+1].nil?
-
x = @x;
-
ws = @ws;
-
if (ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
AddPage(@cur_orientation);
-
@x = x;
-
if (ws > 0)
-
@ws = ws;
-
out(sprintf('%.3f Tw', ws * k));
-
end
-
else
-
@page += 1;
-
@y=@t_margin;
-
end
-
end
-
-
if (w == 0)
-
w = @w - @r_margin - @x;
-
end
-
s = '';
-
if ((fill.to_i == 1) or (border.to_i == 1))
-
if (fill.to_i == 1)
-
op = (border.to_i == 1) ? 'B' : 'f';
-
else
-
op = 'S';
-
end
-
s = sprintf('%.2f %.2f %.2f %.2f re %s ', @x * k, (@h - @y) * k, w * k, -h * k, op);
-
end
-
if (border.is_a?(String))
-
x=@x;
-
y=@y;
-
if (border.include?('L'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ', x*k,(@h-y)*k, x*k,(@h-(y+h))*k);
-
end
-
if (border.include?('T'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ', x*k,(@h-y)*k,(x+w)*k,(@h-y)*k);
-
end
-
if (border.include?('R'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ',(x+w)*k,(@h-y)*k,(x+w)*k,(@h-(y+h))*k);
-
end
-
if (border.include?('B'))
-
s<<sprintf('%.2f %.2f m %.2f %.2f l S ', x*k,(@h-(y+h))*k,(x+w)*k,(@h-(y+h))*k);
-
end
-
end
-
if (txt != '')
-
width = GetStringWidth(txt);
-
if (align == 'R' || align == 'right')
-
dx = w - @c_margin - width;
-
elsif (align=='C' || align == 'center')
-
dx = (w - width)/2;
-
else
-
dx = @c_margin;
-
end
-
if (@color_flag)
-
s << 'q ' + @text_color + ' ';
-
end
-
txt2 = escapetext(txt);
-
s<<sprintf('BT %.2f %.2f Td (%s) Tj ET', (@x + dx) * k, (@h - (@y + 0.5 * h + 0.3 * @font_size)) * k, txt2);
-
if (@underline)
-
s<<' ' + dolinetxt(@x + dx, @y + 0.5 * h + 0.3 * @font_size, txt);
-
end
-
if (@deleted)
-
s<<' ' + dolinetxt(@x + dx, @y + 0.3 * h + 0.2 * @font_size, txt);
-
end
-
if (@color_flag)
-
s<<' Q';
-
end
-
if link && !link.empty?
-
Link(@x + dx, @y + 0.5 * h - 0.5 * @font_size, width, @font_size, link);
-
end
-
end
-
if (s)
-
out(s);
-
end
-
@lasth = h;
-
if (ln.to_i>0)
-
# Go to next line
-
@y += h;
-
if (ln == 1)
-
@x = @l_margin;
-
end
-
else
-
@x += w;
-
end
-
end
-
1
alias_method :cell, :Cell
-
-
#
-
# This method allows printing text with line breaks. They can be automatic (as soon as the text reaches the right border of the cell) or explicit (via the \n character). As many cells as necessary are output, one below the other.<br />
-
# Text can be aligned, centered or justified. The cell block can be framed and the background painted.
-
# @param float :w Width of cells. If 0, they extend up to the right margin of the page.
-
# @param float :h Height of cells.
-
# @param string :txt String to print
-
# @param mixed :border Indicates if borders must be drawn around the cell block. The value can be either a number:<ul><li>0: no border (default)</li><li>1: frame</li></ul>or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
-
# @param string :align Allows to center or align the text. Possible values are:<ul><li>L or empty string: left align</li><li>C: center</li><li>R: right align</li><li>J: justification (default value)</li></ul>
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @param int :ln Indicates where the current position should go after the call. Possible values are:<ul><li>0: to the right</li><li>1: to the beginning of the next line [DEFAULT]</li><li>2: below</li></ul>
-
# @since 1.3
-
# @see SetFont(), SetDrawColor(), SetFillColor(), SetTextColor(), SetLineWidth(), Cell(), Write(), SetAutoPageBreak()
-
#
-
1
def MultiCell(w, h, txt, border=0, align='J', fill=0, ln=1)
-
-
# save current position
-
prevx = @x;
-
prevy = @y;
-
prevpage = @page;
-
-
#Output text with automatic or explicit line breaks
-
-
if (w == 0)
-
w = @w - @r_margin - @x;
-
end
-
-
wmax = (w - 3 * @c_margin);
-
-
s = txt.gsub("\r", ''); # remove carriage returns
-
nb = s.length;
-
-
b=0;
-
if (border)
-
if (border==1)
-
border='LTRB';
-
b='LRT';
-
b2='LR';
-
elsif border.is_a?(String)
-
b2='';
-
if (border.include?('L'))
-
b2<<'L';
-
end
-
if (border.include?('R'))
-
b2<<'R';
-
end
-
b=(border.include?('T')) ? b2 + 'T' : b2;
-
end
-
end
-
sep=-1;
-
to_index=0;
-
from_j=0;
-
l=0;
-
ns=0;
-
nl=1;
-
-
while to_index < nb
-
#Get next character
-
c = s[to_index];
-
if c == "\n"[0]
-
#Explicit line break
-
if @ws > 0
-
@ws = 0
-
out('0 Tw')
-
end
-
#Ed Moss - change begin
-
end_i = to_index == 0 ? 0 : to_index - 1
-
# Changed from s[from_j..to_index] to fix bug reported by Hans Allis.
-
from_j = to_index == 0 ? 1 : from_j
-
Cell(w, h, s[from_j..end_i], b, 2, align, fill)
-
#change end
-
to_index += 1
-
sep=-1
-
from_j=to_index
-
l=0
-
ns=0
-
nl += 1
-
b = b2 if border and nl==2
-
next
-
end
-
if (c == " "[0])
-
sep = to_index;
-
ls = l;
-
ns += 1;
-
end
-
-
l = GetStringWidth(s[from_j, to_index - from_j]);
-
-
if (l > wmax)
-
#Automatic line break
-
if (sep == -1)
-
if (to_index == from_j)
-
to_index += 1;
-
end
-
if (@ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
Cell(w, h, s[from_j..to_index-1], b, 2, align, fill) # my FPDF version
-
else
-
if (align=='J' || align=='justify' || align=='justified')
-
@ws = (ns>1) ? (wmax-ls)/(ns-1) : 0;
-
out(sprintf('%.3f Tw', @ws * @k));
-
end
-
Cell(w, h, s[from_j..sep], b, 2, align, fill);
-
to_index = sep + 1;
-
end
-
sep=-1;
-
from_j = to_index;
-
l=0;
-
ns=0;
-
nl += 1;
-
if (border and (nl==2))
-
b = b2;
-
end
-
else
-
to_index += 1;
-
end
-
end
-
#Last chunk
-
if (@ws>0)
-
@ws=0;
-
out('0 Tw');
-
end
-
if (border.is_a?(String) and border.include?('B'))
-
b<<'B';
-
end
-
Cell(w, h, s[from_j, to_index-from_j], b, 2, align, fill);
-
-
# move cursor to specified position
-
# since 2007-03-03
-
if (ln == 1)
-
# go to the beginning of the next line
-
@x = @l_margin;
-
elsif (ln == 0)
-
# go to the top-right of the cell
-
@page = prevpage;
-
@y = prevy;
-
@x = prevx + w;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell
-
@x = prevx;
-
end
-
end
-
1
alias_method :multi_cell, :MultiCell
-
-
#
-
# This method prints text from the current position. When the right margin is reached (or the \n character is met) a line break occurs and text continues from the left margin. Upon method exit, the current position is left just at the end of the text. It is possible to put a link on the text.<br />
-
# <b>Example:</b><br />
-
# <pre>
-
# #Begin with regular font
-
# :pdf->SetFont('Arial','',14);
-
# :pdf->Write(5,'Visit ');
-
# #Then put a blue underlined link
-
# :pdf->SetTextColor(0,0,255);
-
# :pdf->SetFont('','U');
-
# :pdf->Write(5,'www.tecnick.com','http://www.tecnick.com');
-
# </pre>
-
# @param float :h Line height
-
# @param string :txt String to print
-
# @param mixed :link URL or identifier returned by AddLink()
-
# @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0.
-
# @since 1.5
-
# @see SetFont(), SetTextColor(), AddLink(), MultiCell(), SetAutoPageBreak()
-
#
-
1
def Write(h, txt, link=nil, fill=0)
-
-
#Output text in flowing mode
-
w = @w - @r_margin - @x;
-
wmax = (w - 3 * @c_margin);
-
-
s = txt.gsub("\r", '');
-
nb = s.length;
-
-
# handle single space character
-
if ((nb==1) and (s == " "))
-
@x += GetStringWidth(s);
-
return;
-
end
-
-
sep=-1;
-
i=0;
-
j=0;
-
l=0;
-
nl=1;
-
while(i<nb)
-
#Get next character
-
c = s[i];
-
if (c == "\n"[0])
-
#Explicit line break
-
Cell(w, h, s[j,i-j], 0, 2, '', fill, link);
-
i += 1;
-
sep = -1;
-
j = i;
-
l = 0;
-
if (nl == 1)
-
@x = @l_margin;
-
w = @w - @r_margin - @x;
-
wmax = (w - 3 * @c_margin);
-
end
-
nl += 1;
-
next
-
end
-
if (c == " "[0])
-
sep= i;
-
end
-
l = GetStringWidth(s[j, i - j]);
-
if (l > wmax)
-
#Automatic line break (word wrapping)
-
if (sep == -1)
-
if (@x > @l_margin)
-
#Move to next line
-
@x = @l_margin;
-
@y += h;
-
w=@w - @r_margin - @x;
-
wmax=(w - 3 * @c_margin);
-
i += 1
-
nl += 1
-
next
-
end
-
if (i == j)
-
i += 1
-
end
-
Cell(w, h, s[j, (i-1)], 0, 2, '', fill, link);
-
else
-
Cell(w, h, s[j, (sep-j)], 0, 2, '', fill, link);
-
i = sep+1;
-
end
-
sep = -1;
-
j = i;
-
l = 0;
-
if (nl==1)
-
@x = @l_margin;
-
w = @w - @r_margin - @x;
-
wmax = (w - 3 * @c_margin);
-
end
-
nl += 1;
-
else
-
i += 1;
-
end
-
end
-
#Last chunk
-
if (i != j)
-
Cell(GetStringWidth(s[j..i]), h, s[j..i], 0, 0, '', fill, link);
-
end
-
end
-
1
alias_method :write, :Write
-
-
#
-
# Puts an image in the page. The upper-left corner must be given. The dimensions can be specified in different ways:<ul><li>explicit width and height (expressed in user unit)</li><li>one explicit dimension, the other being calculated automatically in order to keep the original proportions</li><li>no explicit dimension, in which case the image is put at 72 dpi</li></ul>
-
# Supported formats are JPEG and PNG.
-
# For JPEG, all flavors are allowed:<ul><li>gray scales</li><li>true colors (24 bits)</li><li>CMYK (32 bits)</li></ul>
-
# For PNG, are allowed:<ul><li>gray scales on at most 8 bits (256 levels)</li><li>indexed colors</li><li>true colors (24 bits)</li></ul>
-
# but are not supported:<ul><li>Interlacing</li><li>Alpha channel</li></ul>
-
# If a transparent color is defined, it will be taken into account (but will be only interpreted by Acrobat 4 and above).<br />
-
# The format can be specified explicitly or inferred from the file extension.<br />
-
# It is possible to put a link on the image.<br />
-
# Remark: if an image is used several times, only one copy will be embedded in the file.<br />
-
# @param string :file Name of the file containing the image.
-
# @param float :x Abscissa of the upper-left corner.
-
# @param float :y Ordinate of the upper-left corner.
-
# @param float :w Width of the image in the page. If not specified or equal to zero, it is automatically calculated.
-
# @param float :h Height of the image in the page. If not specified or equal to zero, it is automatically calculated.
-
# @param string :type Image format. Possible values are (case insensitive): JPG, JPEG, PNG. If not specified, the type is inferred from the file extension.
-
# @param mixed :link URL or identifier returned by AddLink().
-
# @since 1.1
-
# @see AddLink()
-
#
-
1
def Image(file, x, y, w=0, h=0, type='', link=nil)
-
#Put an image on the page
-
if (@images[file].nil?)
-
#First use of image, get info
-
if (type == '')
-
pos = File::basename(file).rindex('.');
-
if (pos.nil? or pos == 0)
-
Error('Image file has no extension and no type was specified: ' + file);
-
end
-
pos = file.rindex('.');
-
type = file[pos+1..-1];
-
end
-
type.downcase!
-
if (type == 'jpg' or type == 'jpeg')
-
info=parsejpg(file);
-
elsif (type == 'png' or type == 'gif')
-
img = Magick::ImageList.new(file)
-
img.format = "PNG" # convert to PNG from gif
-
img.opacity = 0 # PNG alpha channel delete
-
File.open( @@k_path_cache + File::basename(file), 'w'){|f|
-
f.binmode
-
f.print img.to_blob
-
f.close
-
}
-
info=parsepng( @@k_path_cache + File::basename(file));
-
File.delete( @@k_path_cache + File::basename(file))
-
else
-
#Allow for additional formats
-
mtd='parse' + type;
-
if (!self.respond_to?(mtd))
-
Error('Unsupported image type: ' + type);
-
end
-
info=send(mtd, file);
-
end
-
info['i']=@images.length+1;
-
@images[file] = info;
-
else
-
info=@images[file];
-
end
-
#Automatic width and height calculation if needed
-
if ((w == 0) and (h == 0))
-
rescale_x = (@w - @r_margin - x) / (info['w'] / (@img_scale * @k))
-
rescale_x = 1 if rescale_x >= 1
-
if (y + info['h'] * rescale_x / (@img_scale * @k) > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
#Automatic page break
-
if @pages[@page+1].nil?
-
ws = @ws;
-
if (ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
AddPage(@cur_orientation);
-
if (ws > 0)
-
@ws = ws;
-
out(sprintf('%.3f Tw', ws * @k));
-
end
-
else
-
@page += 1;
-
end
-
y=@t_margin;
-
end
-
rescale_y = (@page_break_trigger - y) / (info['h'] / (@img_scale * @k))
-
rescale_y = 1 if rescale_y >= 1
-
rescale = rescale_y >= rescale_x ? rescale_x : rescale_y
-
-
#Put image at 72 dpi
-
# 2004-06-14 :: Nicola Asuni, scale factor where added
-
w = info['w'] * rescale / (@img_scale * @k);
-
h = info['h'] * rescale / (@img_scale * @k);
-
elsif (w == 0)
-
w = h * info['w'] / info['h'];
-
elsif (h == 0)
-
h = w * info['h'] / info['w'];
-
end
-
out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q', w*@k, h*@k, x*@k, (@h-(y+h))*@k, info['i']));
-
if (link)
-
Link(x, y, w, h, link);
-
end
-
-
#2002-07-31 - Nicola Asuni
-
# set right-bottom corner coordinates
-
@img_rb_x = x + w;
-
@img_rb_y = y + h;
-
end
-
1
alias_method :image, :Image
-
-
#
-
# Performs a line break. The current abscissa goes back to the left margin and the ordinate increases by the amount passed in parameter.
-
# @param float :h The height of the break. By default, the value equals the height of the last printed cell.
-
# @since 1.0
-
# @see Cell()
-
#
-
1
def Ln(h='')
-
#Line feed; default value is last cell height
-
@x=@l_margin;
-
if (h.is_a?(String))
-
@y += @lasth;
-
else
-
@y += h;
-
end
-
-
k=@k;
-
if (@y > @page_break_trigger and !@in_footer and AcceptPageBreak())
-
#Automatic page break
-
if @pages[@page+1].nil?
-
x = @x;
-
ws = @ws;
-
if (ws > 0)
-
@ws = 0;
-
out('0 Tw');
-
end
-
AddPage(@cur_orientation);
-
@x = x;
-
if (ws > 0)
-
@ws = ws;
-
out(sprintf('%.3f Tw', ws * k));
-
end
-
else
-
@page += 1;
-
@y=@t_margin;
-
end
-
end
-
-
end
-
1
alias_method :ln, :Ln
-
-
#
-
# Returns the abscissa of the current position.
-
# @return float
-
# @since 1.2
-
# @see SetX(), GetY(), SetY()
-
#
-
1
def GetX()
-
#Get x position
-
return @x;
-
end
-
1
alias_method :get_x, :GetX
-
-
#
-
# Defines the abscissa of the current position. If the passed value is negative, it is relative to the right of the page.
-
# @param float :x The value of the abscissa.
-
# @since 1.2
-
# @see GetX(), GetY(), SetY(), SetXY()
-
#
-
1
def SetX(x)
-
#Set x position
-
if (x>=0)
-
@x = x;
-
else
-
@x=@w+x;
-
end
-
end
-
1
alias_method :set_x, :SetX
-
-
#
-
# Returns the ordinate of the current position.
-
# @return float
-
# @since 1.0
-
# @see SetY(), GetX(), SetX()
-
#
-
1
def GetY()
-
#Get y position
-
return @y;
-
end
-
1
alias_method :get_y, :GetY
-
-
#
-
# Moves the current abscissa back to the left margin and sets the ordinate. If the passed value is negative, it is relative to the bottom of the page.
-
# @param float :y The value of the ordinate.
-
# @since 1.0
-
# @see GetX(), GetY(), SetY(), SetXY()
-
#
-
1
def SetY(y)
-
#Set y position and reset x
-
@x=@l_margin;
-
if (y>=0)
-
@y = y;
-
else
-
@y=@h+y;
-
end
-
end
-
1
alias_method :set_y, :SetY
-
-
#
-
# Defines the abscissa and ordinate of the current position. If the passed values are negative, they are relative respectively to the right and bottom of the page.
-
# @param float :x The value of the abscissa
-
# @param float :y The value of the ordinate
-
# @since 1.2
-
# @see SetX(), SetY()
-
#
-
1
def SetXY(x, y)
-
#Set x and y positions
-
SetY(y);
-
SetX(x);
-
end
-
1
alias_method :set_xy, :SetXY
-
-
#
-
# Send the document to a given destination: string, local file or browser. In the last case, the plug-in may be used (if present) or a download ("Save as" dialog box) may be forced.<br />
-
# The method first calls Close() if necessary to terminate the document.
-
# @param string :name The name of the file. If not given, the document will be sent to the browser (destination I) with the name doc.pdf.
-
# @param string :dest Destination where to send the document. It can take one of the following values:<ul><li>I: send the file inline to the browser. The plug-in is used if available. The name given by name is used when one selects the "Save as" option on the link generating the PDF.</li><li>D: send to the browser and force a file download with the name given by name.</li><li>F: save to a local file with the name given by name.</li><li>S: return the document as a string. name is ignored.</li></ul>If the parameter is not specified but a name is given, destination is F. If no parameter is specified at all, destination is I.<br />
-
# @since 1.0
-
# @see Close()
-
#
-
1
def Output(name='', dest='')
-
#Output PDF to some destination
-
#Finish document if necessary
-
if (@state < 3)
-
Close();
-
end
-
#Normalize parameters
-
# Boolean no longer supported
-
# if (dest.is_a?(Boolean))
-
# dest = dest ? 'D' : 'F';
-
# end
-
dest = dest.upcase
-
if (dest=='')
-
if (name=='')
-
name='doc.pdf';
-
dest='I';
-
else
-
dest='F';
-
end
-
end
-
case (dest)
-
when 'I'
-
# This is PHP specific code
-
##Send to standard output
-
# if (ob_get_contents())
-
# Error('Some data has already been output, can\'t send PDF file');
-
# end
-
# if (php_sapi_name()!='cli')
-
# #We send to a browser
-
# header('Content-Type: application/pdf');
-
# if (headers_sent())
-
# Error('Some data has already been output to browser, can\'t send PDF file');
-
# end
-
# header('Content-Length: ' + @buffer.length);
-
# header('Content-disposition: inline; filename="' + name + '"');
-
# end
-
return @buffer;
-
-
when 'D'
-
# PHP specific
-
#Download file
-
# if (ob_get_contents())
-
# Error('Some data has already been output, can\'t send PDF file');
-
# end
-
# if (!_SERVER['HTTP_USER_AGENT'].nil? && SERVER['HTTP_USER_AGENT'].include?('MSIE'))
-
# header('Content-Type: application/force-download');
-
# else
-
# header('Content-Type: application/octet-stream');
-
# end
-
# if (headers_sent())
-
# Error('Some data has already been output to browser, can\'t send PDF file');
-
# end
-
# header('Content-Length: '+ @buffer.length);
-
# header('Content-disposition: attachment; filename="' + name + '"');
-
return @buffer;
-
-
when 'F'
-
open(name,'wb') do |f|
-
f.write(@buffer)
-
end
-
# PHP code
-
# #Save to local file
-
# f=open(name,'wb');
-
# if (!f)
-
# Error('Unable to create output file: ' + name);
-
# end
-
# fwrite(f,@buffer,@buffer.length);
-
# f.close
-
-
when 'S'
-
#Return as a string
-
return @buffer;
-
else
-
Error('Incorrect output destination: ' + dest);
-
-
end
-
return '';
-
end
-
1
alias_method :output, :Output
-
-
# Protected methods
-
-
#
-
# Check for locale-related bug
-
# @access protected
-
#
-
1
def dochecks()
-
#Check for locale-related bug
-
if (1.1==1)
-
Error('Don\'t alter the locale before including class file');
-
end
-
#Check for decimal separator
-
if (sprintf('%.1f',1.0)!='1.0')
-
setlocale(LC_NUMERIC,'C');
-
end
-
end
-
-
#
-
# Return fonts path
-
# @access protected
-
#
-
1
def getfontpath(file)
-
# Is it in the @@font_path?
-
if @@font_path
-
fpath = File.join @@font_path, file
-
if File.exists?(fpath)
-
return fpath
-
end
-
end
-
# Is it in this plugin's font folder?
-
fpath = File.join File.dirname(__FILE__), 'fonts', file
-
if File.exists?(fpath)
-
return fpath
-
end
-
# Could not find it.
-
nil
-
end
-
-
#
-
# Start document
-
# @access protected
-
#
-
1
def begindoc()
-
#Start document
-
@state=1;
-
out('%PDF-1.3');
-
end
-
-
#
-
# putpages
-
# @access protected
-
#
-
1
def putpages()
-
nb = @page;
-
if (@alias_nb_pages)
-
nbstr = UTF8ToUTF16BE(nb.to_s, false);
-
#Replace number of pages
-
1.upto(nb) do |n|
-
@pages[n].gsub!(@alias_nb_pages, nbstr)
-
end
-
end
-
if @def_orientation=='P'
-
w_pt=@fw_pt
-
h_pt=@fh_pt
-
else
-
w_pt=@fh_pt
-
h_pt=@fw_pt
-
end
-
filter=(@compress) ? '/Filter /FlateDecode ' : ''
-
1.upto(nb) do |n|
-
#Page
-
newobj
-
out('<</Type /Page')
-
out('/Parent 1 0 R')
-
unless @orientation_changes[n].nil?
-
out(sprintf('/MediaBox [0 0 %.2f %.2f]', h_pt, w_pt))
-
end
-
out('/Resources 2 0 R')
-
if @page_links[n]
-
#Links
-
annots='/Annots ['
-
@page_links[n].each do |pl|
-
rect=sprintf('%.2f %.2f %.2f %.2f', pl[0], pl[1], pl[0]+pl[2], pl[1]-pl[3]);
-
annots<<'<</Type /Annot /Subtype /Link /Rect [' + rect + '] /Border [0 0 0] ';
-
if (pl[4].is_a?(String))
-
annots<<'/A <</S /URI /URI (' + escape(pl[4]) + ')>>>>';
-
else
-
l=@links[pl[4]];
-
h=!@orientation_changes[l[0]].nil? ? w_pt : h_pt;
-
annots<<sprintf('/Dest [%d 0 R /XYZ 0 %.2f null]>>',1+2*l[0], h-l[1]*@k);
-
end
-
end
-
out(annots + ']');
-
end
-
out('/Contents ' + (@n+1).to_s + ' 0 R>>');
-
out('endobj');
-
#Page content
-
p=(@compress) ? gzcompress(@pages[n]) : @pages[n];
-
newobj();
-
out('<<' + filter + '/Length '+ p.length.to_s + '>>');
-
putstream(p);
-
out('endobj');
-
end
-
#Pages root
-
@offsets[1]=@buffer.length;
-
out('1 0 obj');
-
out('<</Type /Pages');
-
kids='/Kids [';
-
0.upto(nb) do |i|
-
kids<<(3+2*i).to_s + ' 0 R ';
-
end
-
out(kids + ']');
-
out('/Count ' + nb.to_s);
-
out(sprintf('/MediaBox [0 0 %.2f %.2f]', w_pt, h_pt));
-
out('>>');
-
out('endobj');
-
end
-
-
#
-
# Adds fonts
-
# putfonts
-
# @access protected
-
#
-
1
def putfonts()
-
nf=@n;
-
@diffs.each do |diff|
-
#Encodings
-
newobj();
-
out('<</Type /Encoding /BaseEncoding /WinAnsiEncoding /Differences [' + diff + ']>>');
-
out('endobj');
-
end
-
@font_files.each do |file, info|
-
#Font file embedding
-
newobj();
-
@font_files[file]['n']=@n;
-
font='';
-
open(getfontpath(file),'rb') do |f|
-
font = f.read();
-
end
-
compressed=(file[-2,2]=='.z');
-
if (!compressed && !info['length2'].nil?)
-
header=((font[0][0])==128);
-
if (header)
-
#Strip first binary header
-
font=font[6];
-
end
-
if header && (font[info['length1']][0] == 128)
-
#Strip second binary header
-
font=font[0..info['length1']] + font[info['length1']+6];
-
end
-
end
-
out('<</Length '+ font.length.to_s);
-
if (compressed)
-
out('/Filter /FlateDecode');
-
end
-
out('/Length1 ' + info['length1'].to_s);
-
if (!info['length2'].nil?)
-
out('/Length2 ' + info['length2'].to_s + ' /Length3 0');
-
end
-
out('>>');
-
open(getfontpath(file),'rb') do |f|
-
putstream(font)
-
end
-
out('endobj');
-
end
-
@fonts.each do |k, font|
-
#Font objects
-
@fonts[k]['n']=@n+1;
-
type = font['type'];
-
name = font['name'];
-
if (type=='core')
-
#Standard font
-
newobj();
-
out('<</Type /Font');
-
out('/BaseFont /' + name);
-
out('/Subtype /Type1');
-
if (name!='Symbol' && name!='ZapfDingbats')
-
out('/Encoding /WinAnsiEncoding');
-
end
-
out('>>');
-
out('endobj');
-
elsif type == 'Type0'
-
putType0(font)
-
elsif (type=='Type1' || type=='TrueType')
-
#Additional Type1 or TrueType font
-
newobj();
-
out('<</Type /Font');
-
out('/BaseFont /' + name);
-
out('/Subtype /' + type);
-
out('/FirstChar 32 /LastChar 255');
-
out('/Widths ' + (@n+1).to_s + ' 0 R');
-
out('/FontDescriptor ' + (@n+2).to_s + ' 0 R');
-
if (font['enc'])
-
if (!font['diff'].nil?)
-
out('/Encoding ' + (nf+font['diff']).to_s + ' 0 R');
-
else
-
out('/Encoding /WinAnsiEncoding');
-
end
-
end
-
out('>>');
-
out('endobj');
-
#Widths
-
newobj();
-
cw=font['cw']; # &
-
s='[';
-
32.upto(255) do |i|
-
s << cw[i.chr] + ' ';
-
end
-
out(s + ']');
-
out('endobj');
-
#Descriptor
-
newobj();
-
s='<</Type /FontDescriptor /FontName /' + name;
-
font['desc'].each do |k, v|
-
s<<' /' + k + ' ' + v;
-
end
-
file = font['file'];
-
if (file)
-
s<<' /FontFile' + (type=='Type1' ? '' : '2') + ' ' + @font_files[file]['n'] + ' 0 R';
-
end
-
out(s + '>>');
-
out('endobj');
-
else
-
#Allow for additional types
-
mtd='put' + type.downcase;
-
if (!self.respond_to?(mtd))
-
Error('Unsupported font type: ' + type)
-
else
-
self.send(mtd,font)
-
end
-
end
-
end
-
end
-
-
1
def putType0(font)
-
#Type0
-
newobj();
-
out('<</Type /Font')
-
out('/Subtype /Type0')
-
out('/BaseFont /'+font['name']+'-'+font['cMap'])
-
out('/Encoding /'+font['cMap'])
-
out('/DescendantFonts ['+(@n+1).to_s+' 0 R]')
-
out('>>')
-
out('endobj')
-
#CIDFont
-
newobj()
-
out('<</Type /Font')
-
out('/Subtype /CIDFontType0')
-
out('/BaseFont /'+font['name'])
-
out('/CIDSystemInfo <</Registry (Adobe) /Ordering ('+font['registry']['ordering']+') /Supplement '+font['registry']['supplement'].to_s+'>>')
-
out('/FontDescriptor '+(@n+1).to_s+' 0 R')
-
w='/W [1 ['
-
font['cw'].keys.sort.each {|key|
-
w+=font['cw'][key].to_s + " "
-
# ActionController::Base::logger.debug key.to_s
-
# ActionController::Base::logger.debug font['cw'][key].to_s
-
}
-
out(w+'] 231 325 500 631 [500] 326 389 500]')
-
out('>>')
-
out('endobj')
-
#Font descriptor
-
newobj()
-
out('<</Type /FontDescriptor')
-
out('/FontName /'+font['name'])
-
out('/Flags 6')
-
out('/FontBBox [0 -200 1000 900]')
-
out('/ItalicAngle 0')
-
out('/Ascent 800')
-
out('/Descent -200')
-
out('/CapHeight 800')
-
out('/StemV 60')
-
out('>>')
-
out('endobj')
-
end
-
-
#
-
# putimages
-
# @access protected
-
#
-
1
def putimages()
-
filter=(@compress) ? '/Filter /FlateDecode ' : '';
-
@images.each do |file, info| # was while(list(file, info)=each(@images))
-
newobj();
-
@images[file]['n']=@n;
-
out('<</Type /XObject');
-
out('/Subtype /Image');
-
out('/Width ' + info['w'].to_s);
-
out('/Height ' + info['h'].to_s);
-
if (info['cs']=='Indexed')
-
out('/ColorSpace [/Indexed /DeviceRGB ' + (info['pal'].length/3-1).to_s + ' ' + (@n+1).to_s + ' 0 R]');
-
else
-
out('/ColorSpace /' + info['cs']);
-
if (info['cs']=='DeviceCMYK')
-
out('/Decode [1 0 1 0 1 0 1 0]');
-
end
-
end
-
out('/BitsPerComponent ' + info['bpc'].to_s);
-
if (!info['f'].nil?)
-
out('/Filter /' + info['f']);
-
end
-
if (!info['parms'].nil?)
-
out(info['parms']);
-
end
-
if (!info['trns'].nil? and info['trns'].kind_of?(Array))
-
trns='';
-
0.upto(info['trns'].length) do |i|
-
trns << info['trns'][i] + ' ' + info['trns'][i] + ' ';
-
end
-
out('/Mask [' + trns + ']');
-
end
-
out('/Length ' + info['data'].length.to_s + '>>');
-
putstream(info['data']);
-
@images[file]['data']=nil
-
out('endobj');
-
#Palette
-
if (info['cs']=='Indexed')
-
newobj();
-
pal=(@compress) ? gzcompress(info['pal']) : info['pal'];
-
out('<<' + filter + '/Length ' + pal.length.to_s + '>>');
-
putstream(pal);
-
out('endobj');
-
end
-
end
-
end
-
-
#
-
# putxobjectdict
-
# @access protected
-
#
-
1
def putxobjectdict()
-
@images.each_value do |image|
-
out('/I' + image['i'].to_s + ' ' + image['n'].to_s + ' 0 R');
-
end
-
end
-
-
#
-
# putresourcedict
-
# @access protected
-
#
-
1
def putresourcedict()
-
out('/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]');
-
out('/Font <<');
-
@fonts.each_value do |font|
-
out('/F' + font['i'].to_s + ' ' + font['n'].to_s + ' 0 R');
-
end
-
out('>>');
-
out('/XObject <<');
-
putxobjectdict();
-
out('>>');
-
end
-
-
#
-
# putresources
-
# @access protected
-
#
-
1
def putresources()
-
putfonts();
-
putimages();
-
#Resource dictionary
-
@offsets[2]=@buffer.length;
-
out('2 0 obj');
-
out('<<');
-
putresourcedict();
-
out('>>');
-
out('endobj');
-
end
-
-
#
-
# putinfo
-
# @access protected
-
#
-
1
def putinfo()
-
out('/Producer ' + textstring(PDF_PRODUCER));
-
if (!@title.nil?)
-
out('/Title ' + textstring(@title));
-
end
-
if (!@subject.nil?)
-
out('/Subject ' + textstring(@subject));
-
end
-
if (!@author.nil?)
-
out('/Author ' + textstring(@author));
-
end
-
if (!@keywords.nil?)
-
out('/Keywords ' + textstring(@keywords));
-
end
-
if (!@creator.nil?)
-
out('/Creator ' + textstring(@creator));
-
end
-
out('/CreationDate ' + textstring('D:' + Time.now.strftime('%Y%m%d%H%M%S')));
-
end
-
-
#
-
# putcatalog
-
# @access protected
-
#
-
1
def putcatalog()
-
out('/Type /Catalog');
-
out('/Pages 1 0 R');
-
if (@zoom_mode=='fullpage')
-
out('/OpenAction [3 0 R /Fit]');
-
elsif (@zoom_mode=='fullwidth')
-
out('/OpenAction [3 0 R /FitH null]');
-
elsif (@zoom_mode=='real')
-
out('/OpenAction [3 0 R /XYZ null null 1]');
-
elsif (!@zoom_mode.is_a?(String))
-
out('/OpenAction [3 0 R /XYZ null null ' + (@zoom_mode/100) + ']');
-
end
-
if (@layout_mode=='single')
-
out('/PageLayout /SinglePage');
-
elsif (@layout_mode=='continuous')
-
out('/PageLayout /OneColumn');
-
elsif (@layout_mode=='two')
-
out('/PageLayout /TwoColumnLeft');
-
end
-
end
-
-
#
-
# puttrailer
-
# @access protected
-
#
-
1
def puttrailer()
-
out('/Size ' + (@n+1).to_s);
-
out('/Root ' + @n.to_s + ' 0 R');
-
out('/Info ' + (@n-1).to_s + ' 0 R');
-
end
-
-
#
-
# putheader
-
# @access protected
-
#
-
1
def putheader()
-
out('%PDF-' + @pdf_version);
-
end
-
-
#
-
# enddoc
-
# @access protected
-
#
-
1
def enddoc()
-
putheader();
-
putpages();
-
putresources();
-
#Info
-
newobj();
-
out('<<');
-
putinfo();
-
out('>>');
-
out('endobj');
-
#Catalog
-
newobj();
-
out('<<');
-
putcatalog();
-
out('>>');
-
out('endobj');
-
#Cross-ref
-
o=@buffer.length;
-
out('xref');
-
out('0 ' + (@n+1).to_s);
-
out('0000000000 65535 f ');
-
1.upto(@n) do |i|
-
out(sprintf('%010d 00000 n ',@offsets[i]));
-
end
-
#Trailer
-
out('trailer');
-
out('<<');
-
puttrailer();
-
out('>>');
-
out('startxref');
-
out(o);
-
out('%%EOF');
-
@state=3;
-
end
-
-
#
-
# beginpage
-
# @access protected
-
#
-
1
def beginpage(orientation)
-
@page += 1;
-
@pages[@page]='';
-
@state=2;
-
@x=@l_margin;
-
@y=@t_margin;
-
@font_family='';
-
#Page orientation
-
if (orientation.empty?)
-
orientation=@def_orientation;
-
else
-
orientation.upcase!
-
if (orientation!=@def_orientation)
-
@orientation_changes[@page]=true;
-
end
-
end
-
if (orientation!=@cur_orientation)
-
#Change orientation
-
if (orientation=='P')
-
@w_pt=@fw_pt;
-
@h_pt=@fh_pt;
-
@w=@fw;
-
@h=@fh;
-
else
-
@w_pt=@fh_pt;
-
@h_pt=@fw_pt;
-
@w=@fh;
-
@h=@fw;
-
end
-
@page_break_trigger=@h-@b_margin;
-
@cur_orientation = orientation;
-
end
-
end
-
-
#
-
# End of page contents
-
# @access protected
-
#
-
1
def endpage()
-
@state=1;
-
end
-
-
#
-
# Begin a new object
-
# @access protected
-
#
-
1
def newobj()
-
@n += 1;
-
@offsets[@n]=@buffer.length;
-
out(@n.to_s + ' 0 obj');
-
end
-
-
#
-
# Underline and Deleted text
-
# @access protected
-
#
-
1
def dolinetxt(x, y, txt)
-
up = @current_font['up'];
-
ut = @current_font['ut'];
-
w = GetStringWidth(txt) + @ws * txt.count(' ');
-
sprintf('%.2f %.2f %.2f %.2f re f', x * @k, (@h - (y - up / 1000.0 * @font_size)) * @k, w * @k, -ut / 1000.0 * @font_size_pt);
-
end
-
-
#
-
# Extract info from a JPEG file
-
# @access protected
-
#
-
1
def parsejpg(file)
-
a=getimagesize(file);
-
if (a.empty?)
-
Error('Missing or incorrect image file: ' + file);
-
end
-
if (!a[2].nil? and a[2]!='JPEG')
-
Error('Not a JPEG file: ' + file);
-
end
-
if (a['channels'].nil? or a['channels']==3)
-
colspace='DeviceRGB';
-
elsif (a['channels']==4)
-
colspace='DeviceCMYK';
-
else
-
colspace='DeviceGray';
-
end
-
bpc=!a['bits'].nil? ? a['bits'] : 8;
-
#Read whole file
-
data='';
-
-
open( @@k_path_cache + File::basename(file),'rb') do |f|
-
data<<f.read();
-
end
-
File.delete( @@k_path_cache + File::basename(file))
-
-
return {'w' => a[0],'h' => a[1],'cs' => colspace,'bpc' => bpc,'f'=>'DCTDecode','data' => data}
-
end
-
-
#
-
# Extract info from a PNG file
-
# @access protected
-
#
-
1
def parsepng(file)
-
f=open(file,'rb');
-
#Check signature
-
if (f.read(8)!=137.chr + 'PNG' + 13.chr + 10.chr + 26.chr + 10.chr)
-
Error('Not a PNG file: ' + file);
-
end
-
#Read header chunk
-
f.read(4);
-
if (f.read(4)!='IHDR')
-
Error('Incorrect PNG file: ' + file);
-
end
-
w=freadint(f);
-
h=freadint(f);
-
bpc=f.read(1).unpack('C')[0];
-
if (bpc>8)
-
Error('16-bit depth not supported: ' + file);
-
end
-
ct=f.read(1).unpack('C')[0];
-
if (ct==0)
-
colspace='DeviceGray';
-
elsif (ct==2)
-
colspace='DeviceRGB';
-
elsif (ct==3)
-
colspace='Indexed';
-
else
-
Error('Alpha channel not supported: ' + file);
-
end
-
if (f.read(1).unpack('C')[0] != 0)
-
Error('Unknown compression method: ' + file);
-
end
-
if (f.read(1).unpack('C')[0] != 0)
-
Error('Unknown filter method: ' + file);
-
end
-
if (f.read(1).unpack('C')[0] != 0)
-
Error('Interlacing not supported: ' + file);
-
end
-
f.read(4);
-
parms='/DecodeParms <</Predictor 15 /Colors ' + (ct==2 ? 3 : 1).to_s + ' /BitsPerComponent ' + bpc.to_s + ' /Columns ' + w.to_s + '>>';
-
#Scan chunks looking for palette, transparency and image data
-
pal='';
-
trns='';
-
data='';
-
begin
-
n=freadint(f);
-
type=f.read(4);
-
if (type=='PLTE')
-
#Read palette
-
pal=f.read( n);
-
f.read(4);
-
elsif (type=='tRNS')
-
#Read transparency info
-
t=f.read( n);
-
if (ct==0)
-
trns = t[1].unpack('C')[0]
-
elsif (ct==2)
-
trns = t[[1].unpack('C')[0], t[3].unpack('C')[0], t[5].unpack('C')[0]]
-
else
-
pos=t.include?(0.chr);
-
if (pos!=false)
-
trns = [pos]
-
end
-
end
-
f.read(4);
-
elsif (type=='IDAT')
-
#Read image data block
-
data<<f.read( n);
-
f.read(4);
-
elsif (type=='IEND')
-
break;
-
else
-
f.read( n+4);
-
end
-
end while(n)
-
if (colspace=='Indexed' and pal.empty?)
-
Error('Missing palette in ' + file);
-
end
-
f.close
-
return {'w' => w, 'h' => h, 'cs' => colspace, 'bpc' => bpc, 'f'=>'FlateDecode', 'parms' => parms, 'pal' => pal, 'trns' => trns, 'data' => data}
-
end
-
-
#
-
# Read a 4-byte integer from file
-
# @access protected
-
#
-
1
def freadint(f)
-
# Read a 4-byte integer from file
-
a = f.read(4).unpack('N')
-
return a[0]
-
end
-
-
#
-
# Format a text string
-
# @access protected
-
#
-
1
def textstring(s)
-
if (@is_unicode)
-
#Convert string to UTF-16BE
-
s = UTF8ToUTF16BE(s, true);
-
end
-
return '(' + escape(s) + ')';
-
end
-
-
#
-
# Format a text string
-
# @access protected
-
#
-
1
def escapetext(s)
-
if (@is_unicode)
-
#Convert string to UTF-16BE
-
s = UTF8ToUTF16BE(s, false);
-
end
-
return escape(s);
-
end
-
-
#
-
# Add \ before \, ( and )
-
# @access protected
-
#
-
1
def escape(s)
-
# Add \ before \, ( and )
-
s.gsub('\\','\\\\\\').gsub('(','\\(').gsub(')','\\)').gsub(13.chr, '\r')
-
end
-
-
#
-
#
-
# @access protected
-
#
-
1
def putstream(s)
-
out('stream');
-
out(s);
-
out('endstream');
-
end
-
-
#
-
# Add a line to the document
-
# @access protected
-
#
-
1
def out(s)
-
if (@state==2)
-
@pages[@page] << s.to_s + "\n";
-
else
-
@buffer << s.to_s + "\n";
-
end
-
end
-
-
#
-
# Adds unicode fonts.<br>
-
# Based on PDF Reference 1.3 (section 5)
-
# @access protected
-
# @author Nicola Asuni
-
# @since 1.52.0.TC005 (2005-01-05)
-
#
-
1
def puttruetypeunicode(font)
-
# Type0 Font
-
# A composite font composed of other fonts, organized hierarchically
-
newobj();
-
out('<</Type /Font');
-
out('/Subtype /Type0');
-
out('/BaseFont /' + font['name'] + '');
-
out('/Encoding /Identity-H'); #The horizontal identity mapping for 2-byte CIDs; may be used with CIDFonts using any Registry, Ordering, and Supplement values.
-
out('/DescendantFonts [' + (@n + 1).to_s + ' 0 R]');
-
out('/ToUnicode ' + (@n + 2).to_s + ' 0 R');
-
out('>>');
-
out('endobj');
-
-
# CIDFontType2
-
# A CIDFont whose glyph descriptions are based on TrueType font technology
-
newobj();
-
out('<</Type /Font');
-
out('/Subtype /CIDFontType2');
-
out('/BaseFont /' + font['name'] + '');
-
out('/CIDSystemInfo ' + (@n + 2).to_s + ' 0 R');
-
out('/FontDescriptor ' + (@n + 3).to_s + ' 0 R');
-
if (!font['desc']['MissingWidth'].nil?)
-
out('/DW ' + font['desc']['MissingWidth'].to_s + ''); # The default width for glyphs in the CIDFont MissingWidth
-
end
-
w = "";
-
font['cw'].each do |cid, width|
-
w << '' + cid.to_s + ' [' + width.to_s + '] '; # define a specific width for each individual CID
-
end
-
out('/W [' + w + ']'); # A description of the widths for the glyphs in the CIDFont
-
out('/CIDToGIDMap ' + (@n + 4).to_s + ' 0 R');
-
out('>>');
-
out('endobj');
-
-
# ToUnicode
-
# is a stream object that contains the definition of the CMap
-
# (PDF Reference 1.3 chap. 5.9)
-
newobj();
-
out('<</Length 383>>');
-
out('stream');
-
out('/CIDInit /ProcSet findresource begin');
-
out('12 dict begin');
-
out('begincmap');
-
out('/CIDSystemInfo');
-
out('<</Registry (Adobe)');
-
out('/Ordering (UCS)');
-
out('/Supplement 0');
-
out('>> def');
-
out('/CMapName /Adobe-Identity-UCS def');
-
out('/CMapType 2 def');
-
out('1 begincodespacerange');
-
out('<0000> <FFFF>');
-
out('endcodespacerange');
-
out('1 beginbfrange');
-
out('<0000> <FFFF> <0000>');
-
out('endbfrange');
-
out('endcmap');
-
out('CMapName currentdict /CMap defineresource pop');
-
out('end');
-
out('end');
-
out('endstream');
-
out('endobj');
-
-
# CIDSystemInfo dictionary
-
# A dictionary containing entries that define the character collection of the CIDFont.
-
newobj();
-
out('<</Registry (Adobe)'); # A string identifying an issuer of character collections
-
out('/Ordering (UCS)'); # A string that uniquely names a character collection issued by a specific registry
-
out('/Supplement 0'); # The supplement number of the character collection.
-
out('>>');
-
out('endobj');
-
-
# Font descriptor
-
# A font descriptor describing the CIDFont default metrics other than its glyph widths
-
newobj();
-
out('<</Type /FontDescriptor');
-
out('/FontName /' + font['name']);
-
font['desc'].each do |key, value|
-
out('/' + key.to_s + ' ' + value.to_s);
-
end
-
if (font['file'])
-
# A stream containing a TrueType font program
-
out('/FontFile2 ' + @font_files[font['file']]['n'].to_s + ' 0 R');
-
end
-
out('>>');
-
out('endobj');
-
-
# Embed CIDToGIDMap
-
# A specification of the mapping from CIDs to glyph indices
-
newobj();
-
ctgfile = getfontpath(font['ctg'])
-
if (!ctgfile)
-
Error('Font file not found: ' + ctgfile);
-
end
-
size = File.size(ctgfile);
-
out('<</Length ' + size.to_s + '');
-
if (ctgfile[-2,2] == '.z') # check file extension
-
# Decompresses data encoded using the public-domain
-
# zlib/deflate compression method, reproducing the
-
# original text or binary data#
-
out('/Filter /FlateDecode');
-
end
-
out('>>');
-
open(ctgfile, "rb") do |f|
-
putstream(f.read())
-
end
-
out('endobj');
-
end
-
-
#
-
# Converts UTF-8 strings to codepoints array.<br>
-
# Invalid byte sequences will be replaced with 0xFFFD (replacement character)<br>
-
# Based on: http://www.faqs.org/rfcs/rfc3629.html
-
# <pre>
-
# Char. number range | UTF-8 octet sequence
-
# (hexadecimal) | (binary)
-
# --------------------+-----------------------------------------------
-
# 0000 0000-0000 007F | 0xxxxxxx
-
# 0000 0080-0000 07FF | 110xxxxx 10xxxxxx
-
# 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
-
# 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
-
# ---------------------------------------------------------------------
-
#
-
# ABFN notation:
-
# ---------------------------------------------------------------------
-
# UTF8-octets =#( UTF8-char )
-
# UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
-
# UTF8-1 = %x00-7F
-
# UTF8-2 = %xC2-DF UTF8-tail
-
#
-
# UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
-
# %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
-
# UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
-
# %xF4 %x80-8F 2( UTF8-tail )
-
# UTF8-tail = %x80-BF
-
# ---------------------------------------------------------------------
-
# </pre>
-
# @param string :str string to process.
-
# @return array containing codepoints (UTF-8 characters values)
-
# @access protected
-
# @author Nicola Asuni
-
# @since 1.53.0.TC005 (2005-01-05)
-
#
-
1
def UTF8StringToArray(str)
-
if (!@is_unicode)
-
return str; # string is not in unicode
-
end
-
-
unicode = [] # array containing unicode values
-
bytes = [] # array containing single character byte sequences
-
numbytes = 1; # number of octetc needed to represent the UTF-8 character
-
-
str = str.to_s; # force :str to be a string
-
-
str.each_byte do |char|
-
if (bytes.length == 0) # get starting octect
-
if (char <= 0x7F)
-
unicode << char # use the character "as is" because is ASCII
-
numbytes = 1
-
elsif ((char >> 0x05) == 0x06) # 2 bytes character (0x06 = 110 BIN)
-
bytes << ((char - 0xC0) << 0x06)
-
numbytes = 2
-
elsif ((char >> 0x04) == 0x0E) # 3 bytes character (0x0E = 1110 BIN)
-
bytes << ((char - 0xE0) << 0x0C)
-
numbytes = 3
-
elsif ((char >> 0x03) == 0x1E) # 4 bytes character (0x1E = 11110 BIN)
-
bytes << ((char - 0xF0) << 0x12)
-
numbytes = 4
-
else
-
# use replacement character for other invalid sequences
-
unicode << 0xFFFD
-
bytes = []
-
numbytes = 1
-
end
-
elsif ((char >> 0x06) == 0x02) # bytes 2, 3 and 4 must start with 0x02 = 10 BIN
-
bytes << (char - 0x80)
-
if (bytes.length == numbytes)
-
# compose UTF-8 bytes to a single unicode value
-
char = bytes[0]
-
1.upto(numbytes-1) do |j|
-
char += (bytes[j] << ((numbytes - j - 1) * 0x06))
-
end
-
if (((char >= 0xD800) and (char <= 0xDFFF)) or (char >= 0x10FFFF))
-
# The definition of UTF-8 prohibits encoding character numbers between
-
# U+D800 and U+DFFF, which are reserved for use with the UTF-16
-
# encoding form (as surrogate pairs) and do not directly represent
-
# characters
-
unicode << 0xFFFD; # use replacement character
-
else
-
unicode << char; # add char to array
-
end
-
# reset data for next char
-
bytes = []
-
numbytes = 1;
-
end
-
else
-
# use replacement character for other invalid sequences
-
unicode << 0xFFFD;
-
bytes = []
-
numbytes = 1;
-
end
-
end
-
return unicode;
-
end
-
-
#
-
# Converts UTF-8 strings to UTF16-BE.<br>
-
# Based on: http://www.faqs.org/rfcs/rfc2781.html
-
# <pre>
-
# Encoding UTF-16:
-
#
-
# Encoding of a single character from an ISO 10646 character value to
-
# UTF-16 proceeds as follows. Let U be the character number, no greater
-
# than 0x10FFFF.
-
#
-
# 1) If U < 0x10000, encode U as a 16-bit unsigned integer and
-
# terminate.
-
#
-
# 2) Let U' = U - 0x10000. Because U is less than or equal to 0x10FFFF,
-
# U' must be less than or equal to 0xFFFFF. That is, U' can be
-
# represented in 20 bits.
-
#
-
# 3) Initialize two 16-bit unsigned integers, W1 and W2, to 0xD800 and
-
# 0xDC00, respectively. These integers each have 10 bits free to
-
# encode the character value, for a total of 20 bits.
-
#
-
# 4) Assign the 10 high-order bits of the 20-bit U' to the 10 low-order
-
# bits of W1 and the 10 low-order bits of U' to the 10 low-order
-
# bits of W2. Terminate.
-
#
-
# Graphically, steps 2 through 4 look like:
-
# U' = yyyyyyyyyyxxxxxxxxxx
-
# W1 = 110110yyyyyyyyyy
-
# W2 = 110111xxxxxxxxxx
-
# </pre>
-
# @param string :str string to process.
-
# @param boolean :setbom if true set the Byte Order Mark (BOM = 0xFEFF)
-
# @return string
-
# @access protected
-
# @author Nicola Asuni
-
# @since 1.53.0.TC005 (2005-01-05)
-
# @uses UTF8StringToArray
-
#
-
1
def UTF8ToUTF16BE(str, setbom=true)
-
if (!@is_unicode)
-
return str; # string is not in unicode
-
end
-
outstr = ""; # string to be returned
-
unicode = UTF8StringToArray(str); # array containing UTF-8 unicode values
-
numitems = unicode.length;
-
-
if (setbom)
-
outstr << "\xFE\xFF"; # Byte Order Mark (BOM)
-
end
-
unicode.each do |char|
-
if (char == 0xFFFD)
-
outstr << "\xFF\xFD"; # replacement character
-
elsif (char < 0x10000)
-
outstr << (char >> 0x08).chr;
-
outstr << (char & 0xFF).chr;
-
else
-
char -= 0x10000;
-
w1 = 0xD800 | (char >> 0x10);
-
w2 = 0xDC00 | (char & 0x3FF);
-
outstr << (w1 >> 0x08).chr;
-
outstr << (w1 & 0xFF).chr;
-
outstr << (w2 >> 0x08).chr;
-
outstr << (w2 & 0xFF).chr;
-
end
-
end
-
return outstr;
-
end
-
-
# ====================================================
-
-
#
-
# Set header font.
-
# @param array :font font
-
# @since 1.1
-
#
-
1
def SetHeaderFont(font)
-
@header_font = font;
-
end
-
1
alias_method :set_header_font, :SetHeaderFont
-
-
#
-
# Set footer font.
-
# @param array :font font
-
# @since 1.1
-
#
-
1
def SetFooterFont(font)
-
@footer_font = font;
-
end
-
1
alias_method :set_footer_font, :SetFooterFont
-
-
#
-
# Set language array.
-
# @param array :language
-
# @since 1.1
-
#
-
1
def SetLanguageArray(language)
-
@l = language;
-
end
-
1
alias_method :set_language_array, :SetLanguageArray
-
#
-
# Set document barcode.
-
# @param string :bc barcode
-
#
-
1
def SetBarcode(bc="")
-
@barcode = bc;
-
end
-
-
#
-
# Print Barcode.
-
# @param int :x x position in user units
-
# @param int :y y position in user units
-
# @param int :w width in user units
-
# @param int :h height position in user units
-
# @param string :type type of barcode (I25, C128A, C128B, C128C, C39)
-
# @param string :style barcode style
-
# @param string :font font for text
-
# @param int :xres x resolution
-
# @param string :code code to print
-
#
-
1
def writeBarcode(x, y, w, h, type, style, font, xres, code)
-
require(File.dirname(__FILE__) + "/barcode/barcode.rb");
-
require(File.dirname(__FILE__) + "/barcode/i25object.rb");
-
require(File.dirname(__FILE__) + "/barcode/c39object.rb");
-
require(File.dirname(__FILE__) + "/barcode/c128aobject.rb");
-
require(File.dirname(__FILE__) + "/barcode/c128bobject.rb");
-
require(File.dirname(__FILE__) + "/barcode/c128cobject.rb");
-
-
if (code.empty?)
-
return;
-
end
-
-
if (style.empty?)
-
style = BCS_ALIGN_LEFT;
-
style |= BCS_IMAGE_PNG;
-
style |= BCS_TRANSPARENT;
-
#:style |= BCS_BORDER;
-
#:style |= BCS_DRAW_TEXT;
-
#:style |= BCS_STRETCH_TEXT;
-
#:style |= BCS_REVERSE_COLOR;
-
end
-
if (font.empty?) then font = BCD_DEFAULT_FONT; end
-
if (xres.empty?) then xres = BCD_DEFAULT_XRES; end
-
-
scale_factor = 1.5 * xres * @k;
-
bc_w = (w * scale_factor).round #width in points
-
bc_h = (h * scale_factor).round #height in points
-
-
case (type.upcase)
-
when "I25"
-
obj = I25Object.new(bc_w, bc_h, style, code);
-
when "C128A"
-
obj = C128AObject.new(bc_w, bc_h, style, code);
-
when "C128B"
-
obj = C128BObject.new(bc_w, bc_h, style, code);
-
when "C128C"
-
obj = C128CObject.new(bc_w, bc_h, style, code);
-
when "C39"
-
obj = C39Object.new(bc_w, bc_h, style, code);
-
end
-
-
obj.SetFont(font);
-
obj.DrawObject(xres);
-
-
#use a temporary file....
-
tmpName = tempnam(@@k_path_cache,'img');
-
imagepng(obj.getImage(), tmpName);
-
Image(tmpName, x, y, w, h, 'png');
-
obj.DestroyObject();
-
obj = nil
-
unlink(tmpName);
-
end
-
-
#
-
# Returns the PDF data.
-
#
-
1
def GetPDFData()
-
if (@state < 3)
-
Close();
-
end
-
return @buffer;
-
end
-
-
# --- HTML PARSER FUNCTIONS ---
-
-
#
-
# Allows to preserve some HTML formatting.<br />
-
# Supports: h1, h2, h3, h4, h5, h6, b, u, i, a, img, p, br, strong, em, ins, del, font, blockquote, li, ul, ol, hr, td, th, tr, table, sup, sub, small
-
# @param string :html text to display
-
# @param boolean :ln if true add a new line after text (default = true)
-
# @param int :fill Indicates if the background must be painted (1) or transparent (0). Default value: 0.
-
#
-
1
def writeHTML(html, ln=true, fill=0, h=0)
-
-
@lasth = h if h > 0
-
if (@lasth == 0)
-
#set row height
-
@lasth = @font_size * @@k_cell_height_ratio;
-
end
-
-
@href = nil
-
@style = "";
-
@t_cells = [[]];
-
@table_id = 0;
-
-
# pre calculate
-
html.split(/(<[^>]+>)/).each do |element|
-
if "<" == element[0,1]
-
#Tag
-
if (element[1, 1] == '/')
-
closedHTMLTagCalc(element[2..-2].downcase);
-
else
-
#Extract attributes
-
# get tag name
-
tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0}
-
tag = tag[0].to_s.downcase;
-
-
# get attributes
-
attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/)
-
attrs = {}
-
attr_array.each do |name, value|
-
attrs[name.downcase] = value;
-
end
-
openHTMLTagCalc(tag, attrs);
-
end
-
end
-
end
-
@table_id = 0;
-
-
html.split(/(<[A-Za-z!?\/][^>]*?>)/).each do |element|
-
if "<" == element[0,1]
-
#Tag
-
if (element[1, 1] == '/')
-
closedHTMLTagHandler(element[2..-2].downcase);
-
else
-
#Extract attributes
-
# get tag name
-
tag = element.scan(/([a-zA-Z0-9]*)/).flatten.delete_if {|x| x.length == 0}
-
tag = tag[0].to_s.downcase;
-
-
# get attributes
-
attr_array = element.scan(/([^=\s]*)=["\']?([^"\']*)["\']?/)
-
attrs = {}
-
attr_array.each do |name, value|
-
attrs[name.downcase] = value;
-
end
-
openHTMLTagHandler(tag, attrs, fill);
-
end
-
-
else
-
#Text
-
if (@href)
-
element.gsub!(/[\t\r\n\f]/, "");
-
addHtmlLink(@href, element, fill);
-
elsif (@tdbegin)
-
element.gsub!(/[\t\r\n\f]/, "");
-
element.gsub!(/ /, " ");
-
base_page = @page;
-
base_x = @x;
-
base_y = @y;
-
-
MultiCell(@tdwidth, @tdheight, unhtmlentities(element.strip), @tableborder, @tdalign, @tdfill, 1);
-
tr_end = @t_cells[@table_id][@tr_id][@td_id]['j1'] + 1;
-
if @max_td_page[tr_end].nil? or (@max_td_page[tr_end] < @page)
-
@max_td_page[tr_end] = @page
-
@max_td_y[tr_end] = @y
-
elsif (@max_td_page[tr_end] == @page)
-
@max_td_y[tr_end] = @y if @max_td_y[tr_end].nil? or (@max_td_y[tr_end] < @y)
-
end
-
-
@page = base_page;
-
@x = base_x + @tdwidth;
-
@y = base_y;
-
elsif (@pre_state == true and element.length > 0)
-
Write(@lasth, unhtmlentities(element), '', fill);
-
elsif (element.strip.length > 0)
-
element.gsub!(/[\t\r\n\f]/, "");
-
element.gsub!(/ /, " ");
-
Write(@lasth, unhtmlentities(element), '', fill);
-
end
-
end
-
end
-
-
if (ln)
-
Ln(@lasth);
-
end
-
end
-
1
alias_method :write_html, :writeHTML
-
-
#
-
# Prints a cell (rectangular area) with optional borders, background color and html text string. The upper-left corner of the cell corresponds to the current position. After the call, the current position moves to the right or to the next line.<br />
-
# If automatic page breaking is enabled and the cell goes beyond the limit, a page break is done before outputting.
-
# @param float :w Cell width. If 0, the cell extends up to the right margin.
-
# @param float :h Cell minimum height. The cell extends automatically if needed.
-
# @param float :x upper-left corner X coordinate
-
# @param float :y upper-left corner Y coordinate
-
# @param string :html html text to print. Default value: empty string.
-
# @param mixed :border Indicates if borders must be drawn around the cell. The value can be either a number:<ul><li>0: no border (default)</li><li>1: frame</li></ul>or a string containing some or all of the following characters (in any order):<ul><li>L: left</li><li>T: top</li><li>R: right</li><li>B: bottom</li></ul>
-
# @param int :ln Indicates where the current position should go after the call. Possible values are:<ul><li>0: to the right</li><li>1: to the beginning of the next line</li><li>2: below</li></ul>
-
# Putting 1 is equivalent to putting 0 and calling Ln() just after. Default value: 0.
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @see Cell()
-
#
-
1
def writeHTMLCell(w, h, x, y, html='', border=0, ln=1, fill=0)
-
-
if (@lasth == 0)
-
#set row height
-
@lasth = @font_size * @@k_cell_height_ratio;
-
end
-
-
if (x == 0)
-
x = GetX();
-
end
-
if (y == 0)
-
y = GetY();
-
end
-
-
# get current page number
-
pagenum = @page;
-
-
SetX(x);
-
SetY(y);
-
-
if (w == 0)
-
w = @fw - x - @r_margin;
-
end
-
-
b=0;
-
if (border)
-
if (border==1)
-
border='LTRB';
-
b='LRT';
-
b2='LR';
-
elsif border.is_a?(String)
-
b2='';
-
if (border.include?('L'))
-
b2<<'L';
-
end
-
if (border.include?('R'))
-
b2<<'R';
-
end
-
b=(border.include?('T')) ? b2 + 'T' : b2;
-
end
-
end
-
-
# store original margin values
-
l_margin = @l_margin;
-
r_margin = @r_margin;
-
-
# set new margin values
-
SetLeftMargin(x);
-
SetRightMargin(@fw - x - w);
-
-
# calculate remaining vertical space on page
-
restspace = GetPageHeight() - GetY() - GetBreakMargin();
-
-
writeHTML(html, true, fill); # write html text
-
-
currentY = GetY();
-
-
@auto_page_break = false;
-
# check if a new page has been created
-
if (@page > pagenum)
-
# design a cell around the text on first page
-
currentpage = @page;
-
@page = pagenum;
-
SetY(GetPageHeight() - restspace - GetBreakMargin());
-
Cell(w, restspace - 1, "", b, 0, 'L', 0);
-
b = b2;
-
@page += 1;
-
while @page < currentpage
-
SetY(@t_margin); # put cursor at the beginning of text
-
Cell(w, @page_break_trigger - @t_margin, "", b, 0, 'L', 0);
-
@page += 1;
-
end
-
if (border.is_a?(String) and border.include?('B'))
-
b<<'B';
-
end
-
# design a cell around the text on last page
-
SetY(@t_margin); # put cursor at the beginning of text
-
Cell(w, currentY - @t_margin, "", b, 0, 'L', 0);
-
else
-
SetY(y); # put cursor at the beginning of text
-
# design a cell around the text
-
Cell(w, [h, (currentY - y)].max, "", border, 0, 'L', 0);
-
end
-
@auto_page_break = true;
-
-
# restore original margin values
-
SetLeftMargin(l_margin);
-
SetRightMargin(r_margin);
-
-
@lasth = h
-
-
# move cursor to specified position
-
if (ln == 0)
-
# go to the top-right of the cell
-
@x = x + w;
-
@y = y;
-
elsif (ln == 1)
-
# go to the beginning of the next line
-
@x = @l_margin;
-
@y = currentY;
-
elsif (ln == 2)
-
# go to the bottom-left of the cell (below)
-
@x = x;
-
@y = currentY;
-
end
-
end
-
1
alias_method :write_html_cell, :writeHTMLCell
-
-
#
-
# Check html table tag position.
-
#
-
# @param array :table potision array
-
# @param int :current tr tag id number
-
# @param int :current td tag id number
-
# @access private
-
# @return int : next td_id position.
-
# value 0 mean that can use position.
-
#
-
1
def checkTableBlockingCellPosition(table, tr_id, td_id )
-
0.upto(tr_id) do |j|
-
0.upto(@t_cells[table][j].size - 1) do |i|
-
if @t_cells[table][j][i]['i0'] <= td_id and td_id <= @t_cells[table][j][i]['i1']
-
if @t_cells[table][j][i]['j0'] <= tr_id and tr_id <= @t_cells[table][j][i]['j1']
-
return @t_cells[table][j][i]['i1'] - td_id + 1;
-
end
-
end
-
end
-
end
-
return 0;
-
end
-
-
#
-
# Calculate opening tags.
-
#
-
# html table cell array : @t_cells
-
#
-
# i0: table cell start position
-
# i1: table cell end position
-
# j0: table row start position
-
# j1: table row end position
-
#
-
# +------+
-
# |i0,j0 |
-
# | i1,j1|
-
# +------+
-
#
-
# example html:
-
# <table>
-
# <tr><td></td><td></td><td></td></tr>
-
# <tr><td colspan=2></td><td></td></tr>
-
# <tr><td rowspan=2></td><td></td><td></td></tr>
-
# <tr><td></td><td></td></tr>
-
# </table>
-
#
-
# i: 0 1 2
-
# j+----+----+----+
-
# :|0,0 |1,0 |2,0 |
-
# 0| 0,0| 1,0| 2,0|
-
# +----+----+----+
-
# |0,1 |2,1 |
-
# 1| 1,1| 2,1|
-
# +----+----+----+
-
# |0,2 |1,2 |2,2 |
-
# 2| | 1,2| 2,2|
-
# + +----+----+
-
# | |1,3 |2,3 |
-
# 3| 0,3| 1,3| 2,3|
-
# +----+----+----+
-
#
-
# html table cell array :
-
# [[[i0=>0,j0=>0,i1=>0,j1=>0],[i0=>1,j0=>0,i1=>1,j1=>0],[i0=>2,j0=>0,i1=>2,j1=>0]],
-
# [[i0=>0,j0=>1,i1=>1,j1=>1],[i0=>2,j0=>1,i1=>2,j1=>1]],
-
# [[i0=>0,j0=>2,i1=>0,j1=>3],[i0=>1,j0=>2,i1=>1,j1=>2],[i0=>2,j0=>2,i1=>2,j1=>2]]
-
# [[i0=>1,j0=>3,i1=>1,j1=>3],[i0=>2,j0=>3,i1=>2,j1=>3]]]
-
#
-
# @param string :tag tag name (in upcase)
-
# @param string :attr tag attribute (in upcase)
-
# @access private
-
#
-
1
def openHTMLTagCalc(tag, attrs)
-
#Opening tag
-
case (tag)
-
when 'table'
-
@max_table_columns[@table_id] = 0;
-
@t_columns = 0;
-
@tr_id = -1;
-
when 'tr'
-
if @max_table_columns[@table_id] < @t_columns
-
@max_table_columns[@table_id] = @t_columns;
-
end
-
@t_columns = 0;
-
@tr_id += 1;
-
@td_id = -1;
-
@t_cells[@table_id].push []
-
when 'td', 'th'
-
@td_id += 1;
-
if attrs['colspan'].nil? or attrs['colspan'] == ''
-
colspan = 1;
-
else
-
colspan = attrs['colspan'].to_i;
-
end
-
if attrs['rowspan'].nil? or attrs['rowspan'] == ''
-
rowspan = 1;
-
else
-
rowspan = attrs['rowspan'].to_i;
-
end
-
-
i = 0;
-
while true
-
next_i_distance = checkTableBlockingCellPosition(@table_id, @tr_id, @td_id + i);
-
if next_i_distance == 0
-
@t_cells[@table_id][@tr_id].push "i0"=>@td_id + i, "j0"=>@tr_id, "i1"=>(@td_id + i + colspan - 1), "j1"=>@tr_id + rowspan - 1
-
break;
-
end
-
i += next_i_distance;
-
end
-
-
@t_columns += colspan;
-
end
-
end
-
-
#
-
# Calculate closing tags.
-
# @param string :tag tag name (in upcase)
-
# @access private
-
#
-
1
def closedHTMLTagCalc(tag)
-
#Closing tag
-
case (tag)
-
when 'table'
-
if @max_table_columns[@table_id] < @t_columns
-
@max_table_columns[@table_id] = @t_columns;
-
end
-
@table_id += 1;
-
@t_cells.push []
-
end
-
end
-
-
#
-
# Convert to accessible file path
-
# @param string :attrname image file name
-
#
-
1
def getImageFilename( attrname )
-
nil
-
end
-
-
#
-
# Process opening tags.
-
# @param string :tag tag name (in upcase)
-
# @param string :attr tag attribute (in upcase)
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @access private
-
#
-
1
def openHTMLTagHandler(tag, attrs, fill=0)
-
#Opening tag
-
case (tag)
-
when 'pre'
-
@pre_state = true;
-
@l_margin += 5;
-
@r_margin += 5;
-
@x += 5;
-
-
when 'table'
-
if @default_table_columns < @max_table_columns[@table_id]
-
@table_columns = @max_table_columns[@table_id];
-
else
-
@table_columns = @default_table_columns;
-
end
-
@l_margin += 5;
-
@r_margin += 5;
-
@x += 5;
-
-
if attrs['border'].nil? or attrs['border'] == ''
-
@tableborder = 0;
-
else
-
@tableborder = attrs['border'];
-
end
-
@tr_id = -1;
-
@max_td_page[0] = @page;
-
@max_td_y[0] = @y;
-
-
when 'tr', 'td', 'th'
-
if tag == 'th'
-
SetStyle('b', true);
-
@tdalign = "C";
-
end
-
if ((!attrs['width'].nil?) and (attrs['width'] != ''))
-
@tdwidth = (attrs['width'].to_i/4);
-
else
-
@tdwidth = ((@w - @l_margin - @r_margin) / @table_columns);
-
end
-
-
if tag == 'tr'
-
@tr_id += 1;
-
@td_id = -1;
-
else
-
@td_id += 1;
-
@x = @l_margin + @tdwidth * @t_cells[@table_id][@tr_id][@td_id]['i0'];
-
end
-
-
if attrs['colspan'].nil? or attrs['border'] == ''
-
@colspan = 1;
-
else
-
@colspan = attrs['colspan'].to_i;
-
end
-
@tdwidth *= @colspan;
-
if ((!attrs['height'].nil?) and (attrs['height'] != ''))
-
@tdheight=(attrs['height'].to_i / @k);
-
else
-
@tdheight = @lasth;
-
end
-
if ((!attrs['align'].nil?) and (attrs['align'] != ''))
-
case (attrs['align'])
-
when 'center'
-
@tdalign = "C";
-
when 'right'
-
@tdalign = "R";
-
when 'left'
-
@tdalign = "L";
-
end
-
end
-
if ((!attrs['bgcolor'].nil?) and (attrs['bgcolor'] != ''))
-
coul = convertColorHexToDec(attrs['bgcolor']);
-
SetFillColor(coul['R'], coul['G'], coul['B']);
-
@tdfill=1;
-
end
-
@tdbegin=true;
-
-
when 'hr'
-
margin = 1;
-
if ((!attrs['width'].nil?) and (attrs['width'] != ''))
-
hrWidth = attrs['width'];
-
else
-
hrWidth = @w - @l_margin - @r_margin - margin;
-
end
-
SetLineWidth(0.2);
-
Line(@x + margin, @y, @x + hrWidth, @y);
-
Ln();
-
-
when 'strong'
-
SetStyle('b', true);
-
-
when 'em'
-
SetStyle('i', true);
-
-
when 'ins'
-
SetStyle('u', true);
-
-
when 'del'
-
SetStyle('d', true);
-
-
when 'b', 'i', 'u'
-
SetStyle(tag, true);
-
-
when 'a'
-
@href = attrs['href'];
-
-
when 'img'
-
if (!attrs['src'].nil?)
-
# Only generates image include a pdf if RMagick is avalaible
-
unless Object.const_defined?(:Magick)
-
Write(@lasth, attrs['src'], '', fill);
-
return
-
end
-
file = getImageFilename(attrs['src'])
-
if (file.nil?)
-
Write(@lasth, attrs['src'], '', fill);
-
return
-
end
-
-
if (attrs['width'].nil?)
-
attrs['width'] = 0;
-
end
-
if (attrs['height'].nil?)
-
attrs['height'] = 0;
-
end
-
-
begin
-
Image(file, GetX(),GetY(), pixelsToMillimeters(attrs['width']), pixelsToMillimeters(attrs['height']));
-
#SetX(@img_rb_x);
-
SetY(@img_rb_y);
-
rescue => err
-
logger.error "pdf: Image: error: #{err.message}"
-
Write(@lasth, attrs['src'], '', fill);
-
if File.file?( @@k_path_cache + File::basename(file))
-
File.delete( @@k_path_cache + File::basename(file))
-
end
-
end
-
end
-
-
when 'ul', 'ol'
-
if @li_count == 0
-
Ln() if @prevquote_count == @quote_count; # insert Ln for keeping quote lines
-
@prevquote_count = @quote_count;
-
end
-
if @li_state == true
-
Ln();
-
@li_state = false;
-
end
-
if tag == 'ul'
-
@list_ordered[@li_count] = false;
-
else
-
@list_ordered[@li_count] = true;
-
end
-
@list_count[@li_count] = 0;
-
@li_count += 1
-
-
when 'li'
-
Ln() if @li_state == true
-
if (@list_ordered[@li_count - 1])
-
@list_count[@li_count - 1] += 1;
-
@li_spacer = " " * @li_count + (@list_count[@li_count - 1]).to_s + ". ";
-
else
-
#unordered list simbol
-
@li_spacer = " " * @li_count + "- ";
-
end
-
Write(@lasth, @spacer + @li_spacer, '', fill);
-
@li_state = true;
-
-
when 'blockquote'
-
if (@quote_count == 0)
-
SetStyle('i', true);
-
@l_margin += 5;
-
else
-
@l_margin += 5 / 2;
-
end
-
@x = @l_margin;
-
@quote_top[@quote_count] = @y;
-
@quote_page[@quote_count] = @page;
-
@quote_count += 1
-
when 'br'
-
Ln();
-
-
if (@li_spacer.length > 0)
-
@x += GetStringWidth(@li_spacer);
-
end
-
-
when 'p'
-
Ln();
-
0.upto(@quote_count - 1) do |i|
-
if @quote_page[i] == @page;
-
if @quote_top[i] == @y - @lasth; # fix start line
-
@quote_top[i] = @y;
-
end
-
else
-
if @quote_page[i] == @page - 1;
-
@quote_page[i] = @page; # fix start line
-
@quote_top[i] = @t_margin;
-
end
-
end
-
end
-
-
when 'sup'
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt * @@k_small_ratio);
-
SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'sub'
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt * @@k_small_ratio);
-
SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'small'
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt * @@k_small_ratio);
-
SetXY(GetX(), GetY() + ((currentfont_size - @font_size)/3));
-
-
when 'font'
-
if (!attrs['color'].nil? and attrs['color']!='')
-
coul = convertColorHexToDec(attrs['color']);
-
SetTextColor(coul['R'], coul['G'], coul['B']);
-
@issetcolor=true;
-
end
-
if (!attrs['face'].nil? and @fontlist.include?(attrs['face'].downcase))
-
SetFont(attrs['face'].downcase);
-
@issetfont=true;
-
end
-
if (!attrs['size'].nil?)
-
headsize = attrs['size'].to_i;
-
else
-
headsize = 0;
-
end
-
currentfont_size = @font_size;
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt + headsize);
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
-
Ln();
-
headsize = (4 - tag[1,1].to_f) * 2
-
@tempfontsize = @font_size_pt;
-
SetFontSize(@font_size_pt + headsize);
-
SetStyle('b', true);
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
end
-
end
-
-
#
-
# Process closing tags.
-
# @param string :tag tag name (in upcase)
-
# @access private
-
#
-
1
def closedHTMLTagHandler(tag)
-
#Closing tag
-
case (tag)
-
when 'pre'
-
@pre_state = false;
-
@l_margin -= 5;
-
@r_margin -= 5;
-
@x = @l_margin;
-
Ln();
-
-
when 'td','th'
-
@tdbegin = false;
-
@tdwidth = 0;
-
@tdheight = 0;
-
@tdalign = "L";
-
SetStyle('b', false);
-
@tdfill = 0;
-
SetFillColor(@prevfill_color[0], @prevfill_color[1], @prevfill_color[2]);
-
-
when 'tr'
-
@y = @max_td_y[@tr_id + 1];
-
@x = @l_margin;
-
@page = @max_td_page[@tr_id + 1];
-
-
when 'table'
-
# Write Table Line
-
width = (@w - @l_margin - @r_margin) / @table_columns;
-
0.upto(@t_cells[@table_id].size - 1) do |j|
-
0.upto(@t_cells[@table_id][j].size - 1) do |i|
-
@page = @max_td_page[j]
-
i0=@t_cells[@table_id][j][i]['i0'];
-
j0=@t_cells[@table_id][j][i]['j0'];
-
i1=@t_cells[@table_id][j][i]['i1'];
-
j1=@t_cells[@table_id][j][i]['j1'];
-
-
Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j0]) # top
-
if ( @page == @max_td_page[j1 + 1])
-
Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @max_td_y[j1+1]) # left
-
Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @max_td_y[j1+1]) # right
-
else
-
Line(@l_margin + width * i0, @max_td_y[j0], @l_margin + width * i0, @page_break_trigger) # left
-
Line(@l_margin + width * (i1+1), @max_td_y[j0], @l_margin + width * (i1+1), @page_break_trigger) # right
-
@page += 1;
-
while @page < @max_td_page[j1 + 1]
-
Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @page_break_trigger) # left
-
Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @page_break_trigger) # right
-
@page += 1;
-
end
-
Line(@l_margin + width * i0, @t_margin, @l_margin + width * i0, @max_td_y[j1+1]) # left
-
Line(@l_margin + width * (i1+1), @t_margin, @l_margin + width * (i1+1), @max_td_y[j1+1]) # right
-
end
-
Line(@l_margin + width * i0, @max_td_y[j1+1], @l_margin + width * (i1+1), @max_td_y[j1+1]) # bottom
-
end
-
end
-
-
@l_margin -= 5;
-
@r_margin -= 5;
-
@tableborder=0;
-
Ln();
-
@table_id += 1;
-
-
when 'strong'
-
SetStyle('b', false);
-
-
when 'em'
-
SetStyle('i', false);
-
-
when 'ins'
-
SetStyle('u', false);
-
-
when 'del'
-
SetStyle('d', false);
-
-
when 'b', 'i', 'u'
-
SetStyle(tag, false);
-
-
when 'a'
-
@href = nil;
-
-
when 'p'
-
Ln();
-
-
when 'sup'
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetXY(GetX(), GetY() - ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'sub'
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetXY(GetX(), GetY() + ((currentfont_size - @font_size)*(@@k_small_ratio)));
-
-
when 'small'
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetXY(GetX(), GetY() - ((@font_size - currentfont_size)/3));
-
-
when 'font'
-
if (@issetcolor == true)
-
SetTextColor(@prevtext_color[0], @prevtext_color[1], @prevtext_color[2]);
-
end
-
if (@issetfont)
-
@font_family = @prevfont_family;
-
@font_style = @prevfont_style;
-
SetFont(@font_family);
-
@issetfont = false;
-
end
-
currentfont_size = @font_size;
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
#@text_color = @prevtext_color;
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
when 'blockquote'
-
@quote_count -= 1
-
if (@quote_page[@quote_count] == @page)
-
Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @y) # quoto line
-
else
-
cur_page = @page;
-
cur_y = @y;
-
@page = @quote_page[@quote_count];
-
if (@quote_top[@quote_count] < @page_break_trigger)
-
Line(@l_margin - 1, @quote_top[@quote_count], @l_margin - 1, @page_break_trigger) # quoto line
-
end
-
@page += 1;
-
while @page < cur_page
-
Line(@l_margin - 1, @t_margin, @l_margin - 1, @page_break_trigger) # quoto line
-
@page += 1;
-
end
-
@y = cur_y;
-
Line(@l_margin - 1, @t_margin, @l_margin - 1, @y) # quoto line
-
end
-
if (@quote_count <= 0)
-
SetStyle('i', false);
-
@l_margin -= 5;
-
else
-
@l_margin -= 5 / 2;
-
end
-
@x = @l_margin;
-
Ln() if @quote_count == 0
-
-
when 'ul', 'ol'
-
@li_count -= 1
-
if @li_state == true
-
Ln();
-
@li_state = false;
-
end
-
-
when 'li'
-
@li_spacer = "";
-
if @li_state == true
-
Ln();
-
@li_state = false;
-
end
-
-
when 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
-
SetFontSize(@tempfontsize);
-
@tempfontsize = @font_size_pt;
-
SetStyle('b', false);
-
Ln();
-
@lasth = @font_size * @@k_cell_height_ratio;
-
-
if tag == 'h1' or tag == 'h2' or tag == 'h3' or tag == 'h4'
-
margin = 1;
-
hrWidth = @w - @l_margin - @r_margin - margin;
-
if tag == 'h1' or tag == 'h2'
-
SetLineWidth(0.2);
-
else
-
SetLineWidth(0.1);
-
end
-
Line(@x + margin, @y, @x + hrWidth, @y);
-
end
-
end
-
end
-
-
#
-
# Sets font style.
-
# @param string :tag tag name (in lowercase)
-
# @param boolean :enable
-
# @access private
-
#
-
1
def SetStyle(tag, enable)
-
#Modify style and select corresponding font
-
['b', 'i', 'u', 'd'].each do |s|
-
if tag.downcase == s
-
if enable
-
@style << s if ! @style.include?(s)
-
else
-
@style = @style.gsub(s,'')
-
end
-
end
-
end
-
SetFont('', @style);
-
end
-
-
#
-
# Output anchor link.
-
# @param string :url link URL
-
# @param string :name link name
-
# @param int :fill Indicates if the cell background must be painted (1) or transparent (0). Default value: 0.
-
# @access public
-
#
-
1
def addHtmlLink(url, name, fill=0)
-
#Put a hyperlink
-
SetTextColor(0, 0, 255);
-
SetStyle('u', true);
-
Write(@lasth, name, url, fill);
-
SetStyle('u', false);
-
SetTextColor(0);
-
end
-
-
#
-
# Returns an associative array (keys: R,G,B) from
-
# a hex html code (e.g. #3FE5AA).
-
# @param string :color hexadecimal html color [#rrggbb]
-
# @return array
-
# @access private
-
#
-
1
def convertColorHexToDec(color = "#000000")
-
tbl_color = {}
-
tbl_color['R'] = color[1,2].hex.to_i;
-
tbl_color['G'] = color[3,2].hex.to_i;
-
tbl_color['B'] = color[5,2].hex.to_i;
-
return tbl_color;
-
end
-
-
#
-
# Converts pixels to millimeters in 72 dpi.
-
# @param int :px pixels
-
# @return float millimeters
-
# @access private
-
#
-
1
def pixelsToMillimeters(px)
-
return px.to_f * 25.4 / 72;
-
end
-
-
#
-
# Reverse function for htmlentities.
-
# Convert entities in UTF-8.
-
#
-
# @param :text_to_convert Text to convert.
-
# @return string converted
-
#
-
1
def unhtmlentities(string)
-
if @@decoder.nil?
-
CGI.unescapeHTML(string)
-
else
-
@@decoder.decode(string)
-
end
-
end
-
-
end # END OF CLASS
-
-
#TODO 2007-05-25 (EJM) Level=0 -
-
#Handle special IE contype request
-
# if (!_SERVER['HTTP_USER_AGENT'].nil? and (_SERVER['HTTP_USER_AGENT']=='contype'))
-
# header('Content-Type: application/pdf');
-
# exit;
-
# }
-
# vim:ts=4:sw=4:
-
# = RedCloth - Textile and Markdown Hybrid for Ruby
-
#
-
# Homepage:: http://whytheluckystiff.net/ruby/redcloth/
-
# Author:: why the lucky stiff (http://whytheluckystiff.net/)
-
# Copyright:: (cc) 2004 why the lucky stiff (and his puppet organizations.)
-
# License:: BSD
-
#
-
# (see http://hobix.com/textile/ for a Textile Reference.)
-
#
-
# Based on (and also inspired by) both:
-
#
-
# PyTextile: http://diveintomark.org/projects/textile/textile.py.txt
-
# Textism for PHP: http://www.textism.com/tools/textile/
-
#
-
#
-
-
# = RedCloth
-
#
-
# RedCloth is a Ruby library for converting Textile and/or Markdown
-
# into HTML. You can use either format, intermingled or separately.
-
# You can also extend RedCloth to honor your own custom text stylings.
-
#
-
# RedCloth users are encouraged to use Textile if they are generating
-
# HTML and to use Markdown if others will be viewing the plain text.
-
#
-
# == What is Textile?
-
#
-
# Textile is a simple formatting style for text
-
# documents, loosely based on some HTML conventions.
-
#
-
# == Sample Textile Text
-
#
-
# h2. This is a title
-
#
-
# h3. This is a subhead
-
#
-
# This is a bit of paragraph.
-
#
-
# bq. This is a blockquote.
-
#
-
# = Writing Textile
-
#
-
# A Textile document consists of paragraphs. Paragraphs
-
# can be specially formatted by adding a small instruction
-
# to the beginning of the paragraph.
-
#
-
# h[n]. Header of size [n].
-
# bq. Blockquote.
-
# # Numeric list.
-
# * Bulleted list.
-
#
-
# == Quick Phrase Modifiers
-
#
-
# Quick phrase modifiers are also included, to allow formatting
-
# of small portions of text within a paragraph.
-
#
-
# \_emphasis\_
-
# \_\_italicized\_\_
-
# \*strong\*
-
# \*\*bold\*\*
-
# ??citation??
-
# -deleted text-
-
# +inserted text+
-
# ^superscript^
-
# ~subscript~
-
# @code@
-
# %(classname)span%
-
#
-
# ==notextile== (leave text alone)
-
#
-
# == Links
-
#
-
# To make a hypertext link, put the link text in "quotation
-
# marks" followed immediately by a colon and the URL of the link.
-
#
-
# Optional: text in (parentheses) following the link text,
-
# but before the closing quotation mark, will become a Title
-
# attribute for the link, visible as a tool tip when a cursor is above it.
-
#
-
# Example:
-
#
-
# "This is a link (This is a title) ":http://www.textism.com
-
#
-
# Will become:
-
#
-
# <a href="http://www.textism.com" title="This is a title">This is a link</a>
-
#
-
# == Images
-
#
-
# To insert an image, put the URL for the image inside exclamation marks.
-
#
-
# Optional: text that immediately follows the URL in (parentheses) will
-
# be used as the Alt text for the image. Images on the web should always
-
# have descriptive Alt text for the benefit of readers using non-graphical
-
# browsers.
-
#
-
# Optional: place a colon followed by a URL immediately after the
-
# closing ! to make the image into a link.
-
#
-
# Example:
-
#
-
# !http://www.textism.com/common/textist.gif(Textist)!
-
#
-
# Will become:
-
#
-
# <img src="http://www.textism.com/common/textist.gif" alt="Textist" />
-
#
-
# With a link:
-
#
-
# !/common/textist.gif(Textist)!:http://textism.com
-
#
-
# Will become:
-
#
-
# <a href="http://textism.com"><img src="/common/textist.gif" alt="Textist" /></a>
-
#
-
# == Defining Acronyms
-
#
-
# HTML allows authors to define acronyms via the tag. The definition appears as a
-
# tool tip when a cursor hovers over the acronym. A crucial aid to clear writing,
-
# this should be used at least once for each acronym in documents where they appear.
-
#
-
# To quickly define an acronym in Textile, place the full text in (parentheses)
-
# immediately following the acronym.
-
#
-
# Example:
-
#
-
# ACLU(American Civil Liberties Union)
-
#
-
# Will become:
-
#
-
# <acronym title="American Civil Liberties Union">ACLU</acronym>
-
#
-
# == Adding Tables
-
#
-
# In Textile, simple tables can be added by seperating each column by
-
# a pipe.
-
#
-
# |a|simple|table|row|
-
# |And|Another|table|row|
-
#
-
# Attributes are defined by style definitions in parentheses.
-
#
-
# table(border:1px solid black).
-
# (background:#ddd;color:red). |{}| | | |
-
#
-
# == Using RedCloth
-
#
-
# RedCloth is simply an extension of the String class, which can handle
-
# Textile formatting. Use it like a String and output HTML with its
-
# RedCloth#to_html method.
-
#
-
# doc = RedCloth.new "
-
#
-
# h2. Test document
-
#
-
# Just a simple test."
-
#
-
# puts doc.to_html
-
#
-
# By default, RedCloth uses both Textile and Markdown formatting, with
-
# Textile formatting taking precedence. If you want to turn off Markdown
-
# formatting, to boost speed and limit the processor:
-
#
-
# class RedCloth::Textile.new( str )
-
-
1
class RedCloth3 < String
-
-
1
VERSION = '3.0.4'
-
1
DEFAULT_RULES = [:textile, :markdown]
-
-
#
-
# Two accessor for setting security restrictions.
-
#
-
# This is a nice thing if you're using RedCloth for
-
# formatting in public places (e.g. Wikis) where you
-
# don't want users to abuse HTML for bad things.
-
#
-
# If +:filter_html+ is set, HTML which wasn't
-
# created by the Textile processor will be escaped.
-
#
-
# If +:filter_styles+ is set, it will also disable
-
# the style markup specifier. ('{color: red}')
-
#
-
1
attr_accessor :filter_html, :filter_styles
-
-
#
-
# Accessor for toggling hard breaks.
-
#
-
# If +:hard_breaks+ is set, single newlines will
-
# be converted to HTML break tags. This is the
-
# default behavior for traditional RedCloth.
-
#
-
1
attr_accessor :hard_breaks
-
-
# Accessor for toggling lite mode.
-
#
-
# In lite mode, block-level rules are ignored. This means
-
# that tables, paragraphs, lists, and such aren't available.
-
# Only the inline markup for bold, italics, entities and so on.
-
#
-
# r = RedCloth.new( "And then? She *fell*!", [:lite_mode] )
-
# r.to_html
-
# #=> "And then? She <strong>fell</strong>!"
-
#
-
1
attr_accessor :lite_mode
-
-
#
-
# Accessor for toggling span caps.
-
#
-
# Textile places `span' tags around capitalized
-
# words by default, but this wreaks havoc on Wikis.
-
# If +:no_span_caps+ is set, this will be
-
# suppressed.
-
#
-
1
attr_accessor :no_span_caps
-
-
#
-
# Establishes the markup predence. Available rules include:
-
#
-
# == Textile Rules
-
#
-
# The following textile rules can be set individually. Or add the complete
-
# set of rules with the single :textile rule, which supplies the rule set in
-
# the following precedence:
-
#
-
# refs_textile:: Textile references (i.e. [hobix]http://hobix.com/)
-
# block_textile_table:: Textile table block structures
-
# block_textile_lists:: Textile list structures
-
# block_textile_prefix:: Textile blocks with prefixes (i.e. bq., h2., etc.)
-
# inline_textile_image:: Textile inline images
-
# inline_textile_link:: Textile inline links
-
# inline_textile_span:: Textile inline spans
-
# glyphs_textile:: Textile entities (such as em-dashes and smart quotes)
-
#
-
# == Markdown
-
#
-
# refs_markdown:: Markdown references (for example: [hobix]: http://hobix.com/)
-
# block_markdown_setext:: Markdown setext headers
-
# block_markdown_atx:: Markdown atx headers
-
# block_markdown_rule:: Markdown horizontal rules
-
# block_markdown_bq:: Markdown blockquotes
-
# block_markdown_lists:: Markdown lists
-
# inline_markdown_link:: Markdown links
-
1
attr_accessor :rules
-
-
# Returns a new RedCloth object, based on _string_ and
-
# enforcing all the included _restrictions_.
-
#
-
# r = RedCloth.new( "h1. A <b>bold</b> man", [:filter_html] )
-
# r.to_html
-
# #=>"<h1>A <b>bold</b> man</h1>"
-
#
-
1
def initialize( string, restrictions = [] )
-
2297
restrictions.each { |r| method( "#{ r }=" ).call( true ) }
-
2297
super( string )
-
end
-
-
#
-
# Generates HTML from the Textile contents.
-
#
-
# r = RedCloth.new( "And then? She *fell*!" )
-
# r.to_html( true )
-
# #=>"And then? She <strong>fell</strong>!"
-
#
-
1
def to_html( *rules )
-
2297
rules = DEFAULT_RULES if rules.empty?
-
# make our working copy
-
2297
text = self.dup
-
-
2297
@urlrefs = {}
-
2297
@shelf = []
-
2297
textile_rules = [:block_textile_table, :block_textile_lists,
-
:block_textile_prefix, :inline_textile_image, :inline_textile_link,
-
:inline_textile_code, :inline_textile_span, :glyphs_textile]
-
2297
markdown_rules = [:refs_markdown, :block_markdown_setext, :block_markdown_atx, :block_markdown_rule,
-
:block_markdown_bq, :block_markdown_lists,
-
:inline_markdown_reflink, :inline_markdown_link]
-
2297
@rules = rules.collect do |rule|
-
9188
case rule
-
when :markdown
-
markdown_rules
-
when :textile
-
2297
textile_rules
-
else
-
6891
rule
-
end
-
end.flatten
-
-
# standard clean up
-
2297
incoming_entities text
-
2297
clean_white_space text
-
-
# start processor
-
2297
@pre_list = []
-
2297
rip_offtags text
-
2297
no_textile text
-
2297
escape_html_tags text
-
# need to do this before #hard_break and #blocks
-
2297
block_textile_quotes text unless @lite_mode
-
2297
hard_break text
-
2297
unless @lite_mode
-
2297
refs text
-
2297
blocks text
-
end
-
2297
inline text
-
2297
smooth_offtags text
-
-
2297
retrieve text
-
-
2297
text.gsub!( /<\/?notextile>/, '' )
-
2297
text.gsub!( /x%x%/, '&' )
-
2297
clean_html text if filter_html
-
2297
text.strip!
-
2297
text
-
-
end
-
-
#######
-
1
private
-
#######
-
#
-
# Mapping of 8-bit ASCII codes to HTML numerical entity equivalents.
-
# (from PyTextile)
-
#
-
1
TEXTILE_TAGS =
-
-
[[128, 8364], [129, 0], [130, 8218], [131, 402], [132, 8222], [133, 8230],
-
[134, 8224], [135, 8225], [136, 710], [137, 8240], [138, 352], [139, 8249],
-
[140, 338], [141, 0], [142, 0], [143, 0], [144, 0], [145, 8216], [146, 8217],
-
[147, 8220], [148, 8221], [149, 8226], [150, 8211], [151, 8212], [152, 732],
-
[153, 8482], [154, 353], [155, 8250], [156, 339], [157, 0], [158, 0], [159, 376]].
-
-
collect! do |a, b|
-
32
[a.chr, ( b.zero? and "" or "&#{ b };" )]
-
end
-
-
#
-
# Regular expressions to convert to HTML.
-
#
-
1
A_HLGN = /(?:(?:<>|<|>|\=|[()]+)+)/
-
1
A_VLGN = /[\-^~]/
-
1
C_CLAS = '(?:\([^")]+\))'
-
1
C_LNGE = '(?:\[[^"\[\]]+\])'
-
1
C_STYL = '(?:\{[^"}]+\})'
-
1
S_CSPN = '(?:\\\\\d+)'
-
1
S_RSPN = '(?:/\d+)'
-
1
A = "(?:#{A_HLGN}?#{A_VLGN}?|#{A_VLGN}?#{A_HLGN}?)"
-
1
S = "(?:#{S_CSPN}?#{S_RSPN}|#{S_RSPN}?#{S_CSPN}?)"
-
1
C = "(?:#{C_CLAS}?#{C_STYL}?#{C_LNGE}?|#{C_STYL}?#{C_LNGE}?#{C_CLAS}?|#{C_LNGE}?#{C_STYL}?#{C_CLAS}?)"
-
# PUNCT = Regexp::quote( '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' )
-
1
PUNCT = Regexp::quote( '!"#$%&\'*+,-./:;=?@\\^_`|~' )
-
1
PUNCT_NOQ = Regexp::quote( '!"#$&\',./:;=?@\\`|' )
-
1
PUNCT_Q = Regexp::quote( '*-_+^~%' )
-
1
HYPERLINK = '(\S+?)([^\w\s/;=\?]*?)(?=\s|<|$)'
-
-
# Text markup tags, don't conflict with block tags
-
1
SIMPLE_HTML_TAGS = [
-
'tt', 'b', 'i', 'big', 'small', 'em', 'strong', 'dfn', 'code',
-
'samp', 'kbd', 'var', 'cite', 'abbr', 'acronym', 'a', 'img', 'br',
-
'br', 'map', 'q', 'sub', 'sup', 'span', 'bdo'
-
]
-
-
1
QTAGS = [
-
['**', 'b', :limit],
-
['*', 'strong', :limit],
-
['??', 'cite', :limit],
-
['-', 'del', :limit],
-
['__', 'i', :limit],
-
['_', 'em', :limit],
-
['%', 'span', :limit],
-
['+', 'ins', :limit],
-
['^', 'sup', :limit],
-
['~', 'sub', :limit]
-
]
-
11
QTAGS_JOIN = QTAGS.map {|rc, ht, rtype| Regexp::quote rc}.join('|')
-
-
1
QTAGS.collect! do |rc, ht, rtype|
-
10
rcq = Regexp::quote rc
-
10
re =
-
case rtype
-
when :limit
-
/(^|[>\s\(]) # sta
-
(?!\-\-)
-
10
(#{QTAGS_JOIN}|) # oqs
-
(#{rcq}) # qtag
-
(\w|[^\s].*?[^\s]) # content
-
(?!\-\-)
-
#{rcq}
-
(#{QTAGS_JOIN}|) # oqa
-
(?=[[:punct:]]|<|\s|\)|$)/x
-
else
-
/(#{rcq})
-
(#{C})
-
(?::(\S+))?
-
(\w|[^\s\-].*?[^\s\-])
-
#{rcq}/xm
-
end
-
10
[rc, ht, re, rtype]
-
end
-
-
# Elements to handle
-
1
GLYPHS = [
-
# [ /([^\s\[{(>])?\'([dmst]\b|ll\b|ve\b|\s|:|$)/, '\1’\2' ], # single closing
-
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)\'/, '\1’' ], # single closing
-
# [ /\'(?=[#{PUNCT_Q}]*(s\b|[\s#{PUNCT_NOQ}]))/, '’' ], # single closing
-
# [ /\'/, '‘' ], # single opening
-
# [ /</, '<' ], # less-than
-
# [ />/, '>' ], # greater-than
-
# [ /([^\s\[{(])?"(\s|:|$)/, '\1”\2' ], # double closing
-
# [ /([^\s\[{(>#{PUNCT_Q}][#{PUNCT_Q}]*)"/, '\1”' ], # double closing
-
# [ /"(?=[#{PUNCT_Q}]*[\s#{PUNCT_NOQ}])/, '”' ], # double closing
-
# [ /"/, '“' ], # double opening
-
# [ /\b( )?\.{3}/, '\1…' ], # ellipsis
-
# [ /\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])/, '<acronym title="\2">\1</acronym>' ], # 3+ uppercase acronym
-
# [ /(^|[^"][>\s])([A-Z][A-Z0-9 ]+[A-Z0-9])([^<A-Za-z0-9]|$)/, '\1<span class="caps">\2</span>\3', :no_span_caps ], # 3+ uppercase caps
-
# [ /(\.\s)?\s?--\s?/, '\1—' ], # em dash
-
# [ /\s->\s/, ' → ' ], # right arrow
-
# [ /\s-\s/, ' – ' ], # en dash
-
# [ /(\d+) ?x ?(\d+)/, '\1×\2' ], # dimension sign
-
# [ /\b ?[(\[]TM[\])]/i, '™' ], # trademark
-
# [ /\b ?[(\[]R[\])]/i, '®' ], # registered
-
# [ /\b ?[(\[]C[\])]/i, '©' ] # copyright
-
]
-
-
1
H_ALGN_VALS = {
-
'<' => 'left',
-
'=' => 'center',
-
'>' => 'right',
-
'<>' => 'justify'
-
}
-
-
1
V_ALGN_VALS = {
-
'^' => 'top',
-
'-' => 'middle',
-
'~' => 'bottom'
-
}
-
-
#
-
# Flexible HTML escaping
-
#
-
1
def htmlesc( str, mode=:Quotes )
-
if str
-
str.gsub!( '&', '&' )
-
str.gsub!( '"', '"' ) if mode != :NoQuotes
-
str.gsub!( "'", ''' ) if mode == :Quotes
-
str.gsub!( '<', '<')
-
str.gsub!( '>', '>')
-
end
-
str
-
end
-
-
# Search and replace for Textile glyphs (quotes, dashes, other symbols)
-
1
def pgl( text )
-
#GLYPHS.each do |re, resub, tog|
-
# next if tog and method( tog ).call
-
# text.gsub! re, resub
-
#end
-
8076
text.gsub!(/\b([A-Z][A-Z0-9]{1,})\b(?:[(]([^)]*)[)])/) do |m|
-
"<acronym title=\"#{htmlesc $2}\">#{$1}</acronym>"
-
end
-
end
-
-
# Parses Textile attribute lists and builds an HTML attribute string
-
1
def pba( text_in, element = "" )
-
-
return '' unless text_in
-
-
style = []
-
text = text_in.dup
-
if element == 'td'
-
colspan = $1 if text =~ /\\(\d+)/
-
rowspan = $1 if text =~ /\/(\d+)/
-
style << "vertical-align:#{ v_align( $& ) };" if text =~ A_VLGN
-
end
-
-
if text.sub!( /\{([^"}]*)\}/, '' ) && !filter_styles
-
sanitized = sanitize_styles($1)
-
style << "#{ sanitized };" unless sanitized.blank?
-
end
-
-
lang = $1 if
-
text.sub!( /\[([^)]+?)\]/, '' )
-
-
cls = $1 if
-
text.sub!( /\(([^()]+?)\)/, '' )
-
-
style << "padding-left:#{ $1.length }em;" if
-
text.sub!( /([(]+)/, '' )
-
-
style << "padding-right:#{ $1.length }em;" if text.sub!( /([)]+)/, '' )
-
-
style << "text-align:#{ h_align( $& ) };" if text =~ A_HLGN
-
-
cls, id = $1, $2 if cls =~ /^(.*?)#(.*)$/
-
-
atts = ''
-
atts << " style=\"#{ style.join }\"" unless style.empty?
-
atts << " class=\"#{ cls }\"" unless cls.to_s.empty?
-
atts << " lang=\"#{ lang }\"" if lang
-
atts << " id=\"#{ id }\"" if id
-
atts << " colspan=\"#{ colspan }\"" if colspan
-
atts << " rowspan=\"#{ rowspan }\"" if rowspan
-
-
atts
-
end
-
-
1
STYLES_RE = /^(color|width|height|border|background|padding|margin|font|text)(-[a-z]+)*:\s*((\d+%?|\d+px|\d+(\.\d+)?em|#[0-9a-f]+|[a-z]+)\s*)+$/i
-
-
1
def sanitize_styles(str)
-
styles = str.split(";").map(&:strip)
-
styles.reject! do |style|
-
!style.match(STYLES_RE)
-
end
-
styles.join(";")
-
end
-
-
1
TABLE_RE = /^(?:table(_?#{S}#{A}#{C})\. ?\n)?^(#{A}#{C}\.? ?\|.*?\|)(\n\n|\Z)/m
-
-
# Parses a Textile table block, building HTML from the result.
-
1
def block_textile_table( text )
-
1185
text.gsub!( TABLE_RE ) do |matches|
-
-
tatts, fullrow = $~[1..2]
-
tatts = pba( tatts, 'table' )
-
tatts = shelve( tatts ) if tatts
-
rows = []
-
-
fullrow.each_line do |row|
-
ratts, row = pba( $1, 'tr' ), $2 if row =~ /^(#{A}#{C}\. )(.*)/m
-
cells = []
-
row.split( /(\|)(?![^\[\|]*\]\])/ )[1..-2].each do |cell|
-
next if cell == '|'
-
ctyp = 'd'
-
ctyp = 'h' if cell =~ /^_/
-
-
catts = ''
-
catts, cell = pba( $1, 'td' ), $2 if cell =~ /^(_?#{S}#{A}#{C}\. ?)(.*)/
-
-
catts = shelve( catts ) if catts
-
cells << "\t\t\t<t#{ ctyp }#{ catts }>#{ cell }</t#{ ctyp }>"
-
end
-
ratts = shelve( ratts ) if ratts
-
rows << "\t\t<tr#{ ratts }>\n#{ cells.join( "\n" ) }\n\t\t</tr>"
-
end
-
"\t<table#{ tatts }>\n#{ rows.join( "\n" ) }\n\t</table>\n\n"
-
end
-
end
-
-
1
LISTS_RE = /^([#*]+?#{C} .*?)$(?![^#*])/m
-
1
LISTS_CONTENT_RE = /^([#*]+)(#{A}#{C}) (.*)$/m
-
-
# Parses Textile lists and generates HTML
-
1
def block_textile_lists( text )
-
1185
text.gsub!( LISTS_RE ) do |match|
-
lines = match.split( /\n/ )
-
last_line = -1
-
depth = []
-
lines.each_with_index do |line, line_id|
-
if line =~ LISTS_CONTENT_RE
-
tl,atts,content = $~[1..3]
-
if depth.last
-
if depth.last.length > tl.length
-
(depth.length - 1).downto(0) do |i|
-
break if depth[i].length == tl.length
-
lines[line_id - 1] << "</li>\n\t</#{ lT( depth[i] ) }l>\n\t"
-
depth.pop
-
end
-
end
-
if depth.last and depth.last.length == tl.length
-
lines[line_id - 1] << '</li>'
-
end
-
end
-
unless depth.last == tl
-
depth << tl
-
atts = pba( atts )
-
atts = shelve( atts ) if atts
-
lines[line_id] = "\t<#{ lT(tl) }l#{ atts }>\n\t<li>#{ content }"
-
else
-
lines[line_id] = "\t\t<li>#{ content }"
-
end
-
last_line = line_id
-
-
else
-
last_line = line_id
-
end
-
if line_id - last_line > 1 or line_id == lines.length - 1
-
while v = depth.pop
-
lines[last_line] << "</li>\n\t</#{ lT( v ) }l>"
-
end
-
end
-
end
-
lines.join( "\n" )
-
end
-
end
-
-
1
QUOTES_RE = /(^>+([^\n]*?)(\n|$))+/m
-
1
QUOTES_CONTENT_RE = /^([> ]+)(.*)$/m
-
-
1
def block_textile_quotes( text )
-
2297
text.gsub!( QUOTES_RE ) do |match|
-
lines = match.split( /\n/ )
-
quotes = ''
-
indent = 0
-
lines.each do |line|
-
line =~ QUOTES_CONTENT_RE
-
bq,content = $1, $2
-
l = bq.count('>')
-
if l != indent
-
quotes << ("\n\n" + (l>indent ? '<blockquote>' * (l-indent) : '</blockquote>' * (indent-l)) + "\n\n")
-
indent = l
-
end
-
quotes << (content + "\n")
-
end
-
quotes << ("\n" + '</blockquote>' * indent + "\n\n")
-
quotes
-
end
-
end
-
-
1
CODE_RE = /(\W)
-
@
-
(?:\|(\w+?)\|)?
-
(.+?)
-
@
-
(?=\W)/x
-
-
1
def inline_textile_code( text )
-
2297
text.gsub!( CODE_RE ) do |m|
-
before,lang,code,after = $~[1..4]
-
lang = " lang=\"#{ lang }\"" if lang
-
rip_offtags( "#{ before }<code#{ lang }>#{ code }</code>#{ after }", false )
-
end
-
end
-
-
1
def lT( text )
-
text =~ /\#$/ ? 'o' : 'u'
-
end
-
-
1
def hard_break( text )
-
text.gsub!( /(.)\n(?!\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
-
end
-
-
1
BLOCKS_GROUP_RE = /\n{2,}(?! )/m
-
-
1
def blocks( text, deep_code = false )
-
2297
text.replace( text.split( BLOCKS_GROUP_RE ).collect do |blk|
-
1185
plain = blk !~ /\A[#*> ]/
-
-
# skip blocks that are complex HTML
-
1185
if blk =~ /^<\/?(\w+).*>/ and not SIMPLE_HTML_TAGS.include? $1
-
blk
-
else
-
# search for indentation levels
-
1185
blk.strip!
-
1185
if blk.empty?
-
blk
-
else
-
1185
code_blk = nil
-
1185
blk.gsub!( /((?:\n(?:\n^ +[^\n]*)+)+)/m ) do |iblk|
-
flush_left iblk
-
blocks iblk, plain
-
iblk.gsub( /^(\S)/, "\t\\1" )
-
if plain
-
code_blk = iblk; ""
-
else
-
iblk
-
end
-
end
-
-
1185
block_applied = 0
-
1185
@rules.each do |rule_name|
-
13035
block_applied += 1 if ( rule_name.to_s.match /^block_/ and method( rule_name ).call( blk ) )
-
end
-
1185
if block_applied.zero?
-
1185
if deep_code
-
blk = "\t<pre><code>#{ blk }</code></pre>"
-
else
-
1185
blk = "\t<p>#{ blk }</p>"
-
end
-
end
-
# hard_break blk
-
1185
blk + "\n#{ code_blk }"
-
end
-
end
-
-
end.join( "\n\n" ) )
-
end
-
-
1
def textile_bq( tag, atts, cite, content )
-
cite, cite_title = check_refs( cite )
-
cite = " cite=\"#{ cite }\"" if cite
-
atts = shelve( atts ) if atts
-
"\t<blockquote#{ cite }>\n\t\t<p#{ atts }>#{ content }</p>\n\t</blockquote>"
-
end
-
-
1
def textile_p( tag, atts, cite, content )
-
atts = shelve( atts ) if atts
-
"\t<#{ tag }#{ atts }>#{ content }</#{ tag }>"
-
end
-
-
1
alias textile_h1 textile_p
-
1
alias textile_h2 textile_p
-
1
alias textile_h3 textile_p
-
1
alias textile_h4 textile_p
-
1
alias textile_h5 textile_p
-
1
alias textile_h6 textile_p
-
-
1
def textile_fn_( tag, num, atts, cite, content )
-
atts << " id=\"fn#{ num }\" class=\"footnote\""
-
content = "<sup>#{ num }</sup> #{ content }"
-
atts = shelve( atts ) if atts
-
"\t<p#{ atts }>#{ content }</p>"
-
end
-
-
1
BLOCK_RE = /^(([a-z]+)(\d*))(#{A}#{C})\.(?::(\S+))? (.*)$/m
-
-
1
def block_textile_prefix( text )
-
1185
if text =~ BLOCK_RE
-
tag,tagpre,num,atts,cite,content = $~[1..6]
-
atts = pba( atts )
-
-
# pass to prefix handler
-
replacement = nil
-
if respond_to? "textile_#{ tag }", true
-
replacement = method( "textile_#{ tag }" ).call( tag, atts, cite, content )
-
elsif respond_to? "textile_#{ tagpre }_", true
-
replacement = method( "textile_#{ tagpre }_" ).call( tagpre, num, atts, cite, content )
-
end
-
text.gsub!( $& ) { replacement } if replacement
-
end
-
end
-
-
1
SETEXT_RE = /\A(.+?)\n([=-])[=-]* *$/m
-
1
def block_markdown_setext( text )
-
if text =~ SETEXT_RE
-
tag = if $2 == "="; "h1"; else; "h2"; end
-
blk, cont = "<#{ tag }>#{ $1 }</#{ tag }>", $'
-
blocks cont
-
text.replace( blk + cont )
-
end
-
end
-
-
1
ATX_RE = /\A(\#{1,6}) # $1 = string of #'s
-
[ ]*
-
(.+?) # $2 = Header text
-
[ ]*
-
\#* # optional closing #'s (not counted)
-
$/x
-
1
def block_markdown_atx( text )
-
if text =~ ATX_RE
-
tag = "h#{ $1.length }"
-
blk, cont = "<#{ tag }>#{ $2 }</#{ tag }>\n\n", $'
-
blocks cont
-
text.replace( blk + cont )
-
end
-
end
-
-
1
MARKDOWN_BQ_RE = /\A(^ *> ?.+$(.+\n)*\n*)+/m
-
-
1
def block_markdown_bq( text )
-
text.gsub!( MARKDOWN_BQ_RE ) do |blk|
-
blk.gsub!( /^ *> ?/, '' )
-
flush_left blk
-
blocks blk
-
blk.gsub!( /^(\S)/, "\t\\1" )
-
"<blockquote>\n#{ blk }\n</blockquote>\n\n"
-
end
-
end
-
-
1
MARKDOWN_RULE_RE = /^(#{
-
3
['*', '-', '_'].collect { |ch| ' ?(' + Regexp::quote( ch ) + ' ?){3,}' }.join( '|' )
-
})$/
-
-
1
def block_markdown_rule( text )
-
1185
text.gsub!( MARKDOWN_RULE_RE ) do |blk|
-
"<hr />"
-
end
-
end
-
-
# XXX TODO XXX
-
1
def block_markdown_lists( text )
-
end
-
-
1
def inline_textile_span( text )
-
2297
QTAGS.each do |qtag_rc, ht, qtag_re, rtype|
-
22970
text.gsub!( qtag_re ) do |m|
-
-
case rtype
-
when :limit
-
sta,oqs,qtag,content,oqa = $~[1..6]
-
atts = nil
-
if content =~ /^(#{C})(.+)$/
-
atts, content = $~[1..2]
-
end
-
else
-
qtag,atts,cite,content = $~[1..4]
-
sta = ''
-
end
-
atts = pba( atts )
-
atts = shelve( atts ) if atts
-
-
"#{ sta }#{ oqs }<#{ ht }#{ atts }>#{ content }</#{ ht }>#{ oqa }"
-
-
end
-
end
-
end
-
-
1
LINK_RE = /
-
(
-
([\s\[{(]|[#{PUNCT}])? # $pre
-
" # start
-
(#{C}) # $atts
-
([^"\n]+?) # $text
-
\s?
-
(?:\(([^)]+?)\)(?="))? # $title
-
":
-
( # $url
-
(\/|[a-zA-Z]+:\/\/|www\.|mailto:) # $proto
-
[\w\/]\S+?
-
)
-
(\/)? # $slash
-
([^\w\=\/;\(\)]*?) # $post
-
)
-
(?=<|\s|$)
-
/x
-
#"
-
1
def inline_textile_link( text )
-
2297
text.gsub!( LINK_RE ) do |m|
-
all,pre,atts,text,title,url,proto,slash,post = $~[1..9]
-
if text.include?('<br />')
-
all
-
else
-
url, url_title = check_refs( url )
-
title ||= url_title
-
-
# Idea below : an URL with unbalanced parethesis and
-
# ending by ')' is put into external parenthesis
-
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
-
url=url[0..-2] # discard closing parenth from url
-
post = ")"+post # add closing parenth to post
-
end
-
atts = pba( atts )
-
atts = " href=\"#{ htmlesc url }#{ slash }\"#{ atts }"
-
atts << " title=\"#{ htmlesc title }\"" if title
-
atts = shelve( atts ) if atts
-
-
external = (url =~ /^https?:\/\//) ? ' class="external"' : ''
-
-
"#{ pre }<a#{ atts }#{ external }>#{ text }</a>#{ post }"
-
end
-
end
-
end
-
-
1
MARKDOWN_REFLINK_RE = /
-
\[([^\[\]]+)\] # $text
-
[ ]? # opt. space
-
(?:\n[ ]*)? # one optional newline followed by spaces
-
\[(.*?)\] # $id
-
/x
-
-
1
def inline_markdown_reflink( text )
-
text.gsub!( MARKDOWN_REFLINK_RE ) do |m|
-
text, id = $~[1..2]
-
-
if id.empty?
-
url, title = check_refs( text )
-
else
-
url, title = check_refs( id )
-
end
-
-
atts = " href=\"#{ url }\""
-
atts << " title=\"#{ title }\"" if title
-
atts = shelve( atts )
-
-
"<a#{ atts }>#{ text }</a>"
-
end
-
end
-
-
1
MARKDOWN_LINK_RE = /
-
\[([^\[\]]+)\] # $text
-
\( # open paren
-
[ \t]* # opt space
-
<?(.+?)>? # $href
-
[ \t]* # opt space
-
(?: # whole title
-
(['"]) # $quote
-
(.*?) # $title
-
\3 # matching quote
-
)? # title is optional
-
\)
-
/x
-
-
1
def inline_markdown_link( text )
-
text.gsub!( MARKDOWN_LINK_RE ) do |m|
-
text, url, quote, title = $~[1..4]
-
-
atts = " href=\"#{ url }\""
-
atts << " title=\"#{ title }\"" if title
-
atts = shelve( atts )
-
-
"<a#{ atts }>#{ text }</a>"
-
end
-
end
-
-
1
TEXTILE_REFS_RE = /(^ *)\[([^\[\n]+?)\](#{HYPERLINK})(?=\s|$)/
-
1
MARKDOWN_REFS_RE = /(^ *)\[([^\n]+?)\]:\s+<?(#{HYPERLINK})>?(?:\s+"((?:[^"]|\\")+)")?(?=\s|$)/m
-
-
1
def refs( text )
-
2297
@rules.each do |rule_name|
-
25267
method( rule_name ).call( text ) if rule_name.to_s.match /^refs_/
-
end
-
end
-
-
1
def refs_textile( text )
-
text.gsub!( TEXTILE_REFS_RE ) do |m|
-
flag, url = $~[2..3]
-
@urlrefs[flag.downcase] = [url, nil]
-
nil
-
end
-
end
-
-
1
def refs_markdown( text )
-
text.gsub!( MARKDOWN_REFS_RE ) do |m|
-
flag, url = $~[2..3]
-
title = $~[6]
-
@urlrefs[flag.downcase] = [url, title]
-
nil
-
end
-
end
-
-
1
def check_refs( text )
-
ret = @urlrefs[text.downcase] if text
-
ret || [text, nil]
-
end
-
-
1
IMAGE_RE = /
-
(>|\s|^) # start of line?
-
\! # opening
-
(\<|\=|\>)? # optional alignment atts
-
(#{C}) # optional style,class atts
-
(?:\. )? # optional dot-space
-
([^\s(!]+?) # presume this is the src
-
\s? # optional space
-
(?:\(((?:[^\(\)]|\([^\)]+\))+?)\))? # optional title
-
\! # closing
-
(?::#{ HYPERLINK })? # optional href
-
/x
-
-
1
def inline_textile_image( text )
-
2297
text.gsub!( IMAGE_RE ) do |m|
-
stln,algn,atts,url,title,href,href_a1,href_a2 = $~[1..8]
-
htmlesc title
-
atts = pba( atts )
-
atts = " src=\"#{ htmlesc url.dup }\"#{ atts }"
-
atts << " title=\"#{ title }\"" if title
-
atts << " alt=\"#{ title }\""
-
# size = @getimagesize($url);
-
# if($size) $atts.= " $size[3]";
-
-
href, alt_title = check_refs( href ) if href
-
url, url_title = check_refs( url )
-
-
out = ''
-
out << "<a#{ shelve( " href=\"#{ href }\"" ) }>" if href
-
out << "<img#{ shelve( atts ) } />"
-
out << "</a>#{ href_a1 }#{ href_a2 }" if href
-
-
if algn
-
algn = h_align( algn )
-
if stln == "<p>"
-
out = "<p style=\"float:#{ algn }\">#{ out }"
-
else
-
out = "#{ stln }<div style=\"float:#{ algn }\">#{ out }</div>"
-
end
-
else
-
out = stln + out
-
end
-
-
out
-
end
-
end
-
-
1
def shelve( val )
-
@shelf << val
-
" :redsh##{ @shelf.length }:"
-
end
-
-
1
def retrieve( text )
-
2297
@shelf.each_with_index do |r, i|
-
text.gsub!( " :redsh##{ i + 1 }:", r )
-
end
-
end
-
-
1
def incoming_entities( text )
-
## turn any incoming ampersands into a dummy character for now.
-
## This uses a negative lookahead for alphanumerics followed by a semicolon,
-
## implying an incoming html entity, to be skipped
-
-
2297
text.gsub!( /&(?![#a-z0-9]+;)/i, "x%x%" )
-
end
-
-
1
def no_textile( text )
-
2297
text.gsub!( /(^|\s)==([^=]+.*?)==(\s|$)?/,
-
'\1<notextile>\2</notextile>\3' )
-
2297
text.gsub!( /^ *==([^=]+.*?)==/m,
-
'\1<notextile>\2</notextile>\3' )
-
end
-
-
1
def clean_white_space( text )
-
# normalize line breaks
-
2297
text.gsub!( /\r\n/, "\n" )
-
2297
text.gsub!( /\r/, "\n" )
-
2297
text.gsub!( /\t/, ' ' )
-
2297
text.gsub!( /^ +$/, '' )
-
2297
text.gsub!( /\n{3,}/, "\n\n" )
-
2297
text.gsub!( /"$/, "\" " )
-
-
# if entire document is indented, flush
-
# to the left side
-
2297
flush_left text
-
end
-
-
1
def flush_left( text )
-
2297
indt = 0
-
2297
if text =~ /^ /
-
while text !~ /^ {#{indt}}\S/
-
indt += 1
-
end unless text.empty?
-
if indt.nonzero?
-
text.gsub!( /^ {#{indt}}/, '' )
-
end
-
end
-
end
-
-
1
def footnote_ref( text )
-
8076
text.gsub!( /\b\[([0-9]+?)\](\s)?/,
-
'<sup><a href="#fn\1">\1</a></sup>\2' )
-
end
-
-
1
OFFTAGS = /(code|pre|kbd|notextile)/
-
1
OFFTAG_MATCH = /(?:(<\/#{ OFFTAGS }>)|(<#{ OFFTAGS }[^>]*>))(.*?)(?=<\/?#{ OFFTAGS }\W|\Z)/mi
-
1
OFFTAG_OPEN = /<#{ OFFTAGS }/
-
1
OFFTAG_CLOSE = /<\/?#{ OFFTAGS }/
-
1
HASTAG_MATCH = /(<\/?\w[^\n]*?>)/m
-
1
ALLTAG_MATCH = /(<\/?\w[^\n]*?>)|.*?(?=<\/?\w[^\n]*?>|$)/m
-
-
1
def glyphs_textile( text, level = 0 )
-
9261
if text !~ HASTAG_MATCH
-
8076
pgl text
-
8076
footnote_ref text
-
else
-
1185
codepre = 0
-
1185
text.gsub!( ALLTAG_MATCH ) do |line|
-
## matches are off if we're between <code>, <pre> etc.
-
12670
if $1
-
5706
if line =~ OFFTAG_OPEN
-
codepre += 1
-
elsif line =~ OFFTAG_CLOSE
-
codepre -= 1
-
codepre = 0 if codepre < 0
-
end
-
elsif codepre.zero?
-
6964
glyphs_textile( line, level + 1 )
-
else
-
htmlesc( line, :NoQuotes )
-
end
-
# p [level, codepre, line]
-
-
12670
line
-
end
-
end
-
end
-
-
1
def rip_offtags( text, escape_aftertag=true, escape_line=true )
-
2297
if text =~ /<.*>/
-
## strip and encode <pre> content
-
codepre, used_offtags = 0, {}
-
text.gsub!( OFFTAG_MATCH ) do |line|
-
if $3
-
first, offtag, aftertag = $3, $4, $5
-
codepre += 1
-
used_offtags[offtag] = true
-
if codepre - used_offtags.length > 0
-
htmlesc( line, :NoQuotes ) if escape_line
-
@pre_list.last << line
-
line = ""
-
else
-
### htmlesc is disabled between CODE tags which will be parsed with highlighter
-
### Regexp in formatter.rb is : /<code\s+class="(\w+)">\s?(.+)/m
-
### NB: some changes were made not to use $N variables, because we use "match"
-
### and it breaks following lines
-
htmlesc( aftertag, :NoQuotes ) if aftertag && escape_aftertag && !first.match(/<code\s+class="(\w+)">/)
-
line = "<redpre##{ @pre_list.length }>"
-
first.match(/<#{ OFFTAGS }([^>]*)>/)
-
tag = $1
-
$2.to_s.match(/(class\=("[^"]+"|'[^']+'))/i)
-
tag << " #{$1}" if $1
-
@pre_list << "<#{ tag }>#{ aftertag }"
-
end
-
elsif $1 and codepre > 0
-
if codepre - used_offtags.length > 0
-
htmlesc( line, :NoQuotes ) if escape_line
-
@pre_list.last << line
-
line = ""
-
end
-
codepre -= 1 unless codepre.zero?
-
used_offtags = {} if codepre.zero?
-
end
-
line
-
end
-
end
-
2297
text
-
end
-
-
1
def smooth_offtags( text )
-
unless @pre_list.empty?
-
## replace <pre> content
-
text.gsub!( /<redpre#(\d+)>/ ) { @pre_list[$1.to_i] }
-
end
-
end
-
-
1
def inline( text )
-
2297
[/^inline_/, /^glyphs_/].each do |meth_re|
-
4594
@rules.each do |rule_name|
-
50534
method( rule_name ).call( text ) if rule_name.to_s.match( meth_re )
-
end
-
end
-
end
-
-
1
def h_align( text )
-
H_ALGN_VALS[text]
-
end
-
-
1
def v_align( text )
-
V_ALGN_VALS[text]
-
end
-
-
1
def textile_popup_help( name, windowW, windowH )
-
' <a target="_blank" href="http://hobix.com/textile/#' + helpvar + '" onclick="window.open(this.href, \'popupwindow\', \'width=' + windowW + ',height=' + windowH + ',scrollbars,resizable\'); return false;">' + name + '</a><br />'
-
end
-
-
# HTML cleansing stuff
-
1
BASIC_TAGS = {
-
'a' => ['href', 'title'],
-
'img' => ['src', 'alt', 'title'],
-
'br' => [],
-
'i' => nil,
-
'u' => nil,
-
'b' => nil,
-
'pre' => nil,
-
'kbd' => nil,
-
'code' => ['lang'],
-
'cite' => nil,
-
'strong' => nil,
-
'em' => nil,
-
'ins' => nil,
-
'sup' => nil,
-
'sub' => nil,
-
'del' => nil,
-
'table' => nil,
-
'tr' => nil,
-
'td' => ['colspan', 'rowspan'],
-
'th' => nil,
-
'ol' => nil,
-
'ul' => nil,
-
'li' => nil,
-
'p' => nil,
-
'h1' => nil,
-
'h2' => nil,
-
'h3' => nil,
-
'h4' => nil,
-
'h5' => nil,
-
'h6' => nil,
-
'blockquote' => ['cite']
-
}
-
-
1
def clean_html( text, tags = BASIC_TAGS )
-
text.gsub!( /<!\[CDATA\[/, '' )
-
text.gsub!( /<(\/*)(\w+)([^>]*)>/ ) do
-
raw = $~
-
tag = raw[2].downcase
-
if tags.has_key? tag
-
pcs = [tag]
-
tags[tag].each do |prop|
-
['"', "'", ''].each do |q|
-
q2 = ( q != '' ? q : '\s' )
-
if raw[3] =~ /#{prop}\s*=\s*#{q}([^#{q2}]+)#{q}/i
-
attrv = $1
-
next if prop == 'src' and attrv =~ %r{^(?!http)\w+:}
-
pcs << "#{prop}=\"#{$1.gsub('"', '\\"')}\""
-
break
-
end
-
end
-
end if tags[tag]
-
"<#{raw[1]}#{pcs.join " "}>"
-
else
-
" "
-
end
-
end
-
end
-
-
1
ALLOWED_TAGS = %w(redpre pre code notextile)
-
-
1
def escape_html_tags(text)
-
2297
text.gsub!(%r{<(\/?([!\w]+)[^<>\n]*)(>?)}) {|m| ALLOWED_TAGS.include?($2) ? "<#{$1}#{$3}" : "<#{$1}#{'>' unless $3.blank?}" }
-
end
-
end
-
-
1
require 'redmine/access_control'
-
1
require 'redmine/menu_manager'
-
1
require 'redmine/activity'
-
1
require 'redmine/search'
-
1
require 'redmine/custom_field_format'
-
1
require 'redmine/mime_type'
-
1
require 'redmine/core_ext'
-
1
require 'redmine/themes'
-
1
require 'redmine/hook'
-
1
require 'redmine/plugin'
-
1
require 'redmine/notifiable'
-
1
require 'redmine/wiki_formatting'
-
1
require 'redmine/scm/base'
-
-
1
begin
-
1
require 'RMagick' unless Object.const_defined?(:Magick)
-
rescue LoadError
-
# RMagick is not available
-
end
-
-
1
if RUBY_VERSION < '1.9'
-
require 'fastercsv'
-
else
-
1
require 'csv'
-
1
FCSV = CSV
-
end
-
-
1
Redmine::Scm::Base.add "Subversion"
-
1
Redmine::Scm::Base.add "Darcs"
-
1
Redmine::Scm::Base.add "Mercurial"
-
1
Redmine::Scm::Base.add "Cvs"
-
1
Redmine::Scm::Base.add "Bazaar"
-
1
Redmine::Scm::Base.add "Git"
-
1
Redmine::Scm::Base.add "Filesystem"
-
-
1
Redmine::CustomFieldFormat.map do |fields|
-
1
fields.register 'string'
-
1
fields.register 'text'
-
1
fields.register 'int', :label => :label_integer
-
1
fields.register 'float'
-
1
fields.register 'list'
-
1
fields.register 'date'
-
1
fields.register 'bool', :label => :label_boolean
-
1
fields.register 'user', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
-
1
fields.register 'version', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
-
end
-
-
# Permissions
-
1
Redmine::AccessControl.map do |map|
-
1
map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true
-
1
map.permission :search_project, {:search => :index}, :public => true
-
1
map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
-
1
map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
-
1
map.permission :select_project_modules, {:projects => :modules}, :require => :member
-
1
map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
-
1
map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
-
1
map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
-
-
1
map.project_module :issue_tracking do |map|
-
# Issue categories
-
1
map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
-
# Issues
-
1
map.permission :view_issues, {:issues => [:index, :show],
-
:auto_complete => [:issues],
-
:context_menus => [:issues],
-
:versions => [:index, :show, :status_by],
-
:journals => [:index, :diff],
-
:queries => :index,
-
:reports => [:issue_report, :issue_report_details]}
-
1
map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
-
1
map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
-
1
map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
-
1
map.permission :manage_subtasks, {}
-
1
map.permission :set_issues_private, {}
-
1
map.permission :set_own_issues_private, {}, :require => :loggedin
-
1
map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
-
1
map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
-
1
map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
-
1
map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
-
1
map.permission :delete_issues, {:issues => :destroy}, :require => :member
-
# Queries
-
1
map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
-
1
map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
-
# Watchers
-
1
map.permission :view_issue_watchers, {}
-
1
map.permission :add_issue_watchers, {:watchers => :new}
-
1
map.permission :delete_issue_watchers, {:watchers => :destroy}
-
end
-
-
1
map.project_module :time_tracking do |map|
-
1
map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
-
1
map.permission :view_time_entries, :timelog => [:index, :report, :show]
-
1
map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
-
1
map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
-
1
map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
-
end
-
-
1
map.project_module :news do |map|
-
1
map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
-
1
map.permission :view_news, {:news => [:index, :show]}, :public => true
-
1
map.permission :comment_news, {:comments => :create}
-
end
-
-
1
map.project_module :documents do |map|
-
1
map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
-
1
map.permission :view_documents, :documents => [:index, :show, :download]
-
end
-
-
1
map.project_module :files do |map|
-
1
map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
-
1
map.permission :view_files, :files => :index, :versions => :download
-
end
-
-
1
map.project_module :wiki do |map|
-
1
map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
-
1
map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
-
1
map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
-
1
map.permission :view_wiki_pages, :wiki => [:index, :show, :special, :date_index]
-
1
map.permission :export_wiki_pages, :wiki => [:export]
-
1
map.permission :view_wiki_edits, :wiki => [:history, :diff, :annotate]
-
1
map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
-
1
map.permission :delete_wiki_pages_attachments, {}
-
1
map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
-
end
-
-
1
map.project_module :repository do |map|
-
1
map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
-
1
map.permission :browse_repository, :repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]
-
1
map.permission :view_changesets, :repositories => [:show, :revisions, :revision]
-
1
map.permission :commit_access, {}
-
1
map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
-
end
-
-
1
map.project_module :boards do |map|
-
1
map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
-
1
map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true
-
1
map.permission :add_messages, {:messages => [:new, :reply, :quote]}
-
1
map.permission :edit_messages, {:messages => :edit}, :require => :member
-
1
map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
-
1
map.permission :delete_messages, {:messages => :destroy}, :require => :member
-
1
map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
-
end
-
-
1
map.project_module :calendar do |map|
-
1
map.permission :view_calendar, :calendars => [:show, :update]
-
end
-
-
1
map.project_module :gantt do |map|
-
1
map.permission :view_gantt, :gantts => [:show, :update]
-
end
-
end
-
-
1
Redmine::MenuManager.map :top_menu do |menu|
-
1
menu.push :home, :home_path
-
360
menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
-
1
menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
-
360
menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
-
1
menu.push :help, Redmine::Info.help_url, :last => true
-
end
-
-
1
Redmine::MenuManager.map :account_menu do |menu|
-
360
menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
-
360
menu.push :register, { :controller => 'account', :action => 'register' }, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
-
360
menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
-
360
menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
-
end
-
-
1
Redmine::MenuManager.map :application_menu do |menu|
-
# Empty
-
end
-
-
1
Redmine::MenuManager.map :admin_menu do |menu|
-
1
menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
-
1
menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
-
1
menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
-
1
menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
-
1
menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
-
1
menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
-
:html => {:class => 'issue_statuses'}
-
1
menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
-
1
menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
-
:html => {:class => 'custom_fields'}
-
1
menu.push :enumerations, {:controller => 'enumerations'}
-
1
menu.push :settings, {:controller => 'settings'}
-
1
menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
-
:html => {:class => 'server_authentication'}
-
1
menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
-
1
menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
-
end
-
-
1
Redmine::MenuManager.map :project_menu do |menu|
-
1
menu.push :overview, { :controller => 'projects', :action => 'show' }
-
1
menu.push :activity, { :controller => 'activities', :action => 'index' }
-
1
menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
-
108
:if => Proc.new { |p| p.shared_versions.any? }
-
1
menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
-
1
menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
-
:html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
-
1
menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
-
1
menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
-
1
menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
-
1
menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
-
1
menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
-
108
:if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
-
1
menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
-
108
:if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
-
1
menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
-
1
menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
-
108
:if => Proc.new { |p| p.repository && !p.repository.new_record? }
-
1
menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
-
end
-
-
1
Redmine::Activity.map do |activity|
-
1
activity.register :issues, :class_name => %w(Issue Journal)
-
1
activity.register :changesets
-
1
activity.register :news
-
1
activity.register :documents, :class_name => %w(Document Attachment)
-
1
activity.register :files, :class_name => 'Attachment'
-
1
activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
-
1
activity.register :messages, :default => false
-
1
activity.register :time_entries, :default => false
-
end
-
-
1
Redmine::Search.map do |search|
-
1
search.register :issues
-
1
search.register :news
-
1
search.register :documents
-
1
search.register :changesets
-
1
search.register :wiki_pages
-
1
search.register :messages
-
1
search.register :projects
-
end
-
-
1
Redmine::WikiFormatting.map do |format|
-
1
format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
-
end
-
-
1
ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module AccessControl
-
-
1
class << self
-
1
def map
-
22
mapper = Mapper.new
-
22
yield mapper
-
22
@permissions ||= []
-
22
@permissions += mapper.mapped_permissions
-
end
-
-
1
def permissions
-
68509
@permissions
-
end
-
-
# Returns the permission of given name or nil if it wasn't found
-
# Argument should be a symbol
-
1
def permission(name)
-
2551853
permissions.detect {|p| p.name == name}
-
end
-
-
# Returns the actions that are allowed by the permission of given name
-
1
def allowed_actions(permission_name)
-
67601
perm = permission(permission_name)
-
67601
perm ? perm.actions : []
-
end
-
-
1
def public_permissions
-
4014
@public_permissions ||= @permissions.select {|p| p.public?}
-
end
-
-
1
def members_only_permissions
-
@members_only_permissions ||= @permissions.select {|p| p.require_member?}
-
end
-
-
1
def loggedin_only_permissions
-
@loggedin_only_permissions ||= @permissions.select {|p| p.require_loggedin?}
-
end
-
-
1
def available_project_modules
-
@available_project_modules ||= @permissions.collect(&:project_module).uniq.compact
-
end
-
-
1
def modules_permissions(modules)
-
144669
@permissions.select {|p| p.project_module.nil? || modules.include?(p.project_module.to_s)}
-
end
-
end
-
-
1
class Mapper
-
1
def initialize
-
22
@project_module = nil
-
end
-
-
1
def permission(name, hash, options={})
-
82
@permissions ||= []
-
82
options.merge!(:project_module => @project_module)
-
82
@permissions << Permission.new(name, hash, options)
-
end
-
-
1
def project_module(name, options={})
-
31
@project_module = name
-
31
yield self
-
31
@project_module = nil
-
end
-
-
1
def mapped_permissions
-
22
@permissions
-
end
-
end
-
-
1
class Permission
-
1
attr_reader :name, :actions, :project_module
-
-
1
def initialize(name, hash, options)
-
82
@name = name
-
82
@actions = []
-
82
@public = options[:public] || false
-
82
@require = options[:require]
-
82
@project_module = options[:project_module]
-
82
hash.each do |controller, actions|
-
118
if actions.is_a? Array
-
307
@actions << actions.collect {|action| "#{controller}/#{action}"}
-
else
-
37
@actions << "#{controller}/#{actions}"
-
end
-
end
-
82
@actions.flatten!
-
end
-
-
1
def public?
-
82
@public
-
end
-
-
1
def require_member?
-
@require && @require == :member
-
end
-
-
1
def require_loggedin?
-
@require && (@require == :member || @require == :loggedin)
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module AccessKeys
-
ACCESSKEYS = {:edit => 'e',
-
:preview => 'r',
-
:quick_search => 'f',
-
:search => '4',
-
:new_issue => '7'
-
1
}.freeze unless const_defined?(:ACCESSKEYS)
-
-
1
def self.key_for(action)
-
735
ACCESSKEYS[action]
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Activity
-
-
1
mattr_accessor :available_event_types, :default_event_types, :providers
-
-
1
@@available_event_types = []
-
1
@@default_event_types = []
-
9
@@providers = Hash.new {|h,k| h[k]=[] }
-
-
1
class << self
-
1
def map(&block)
-
1
yield self
-
end
-
-
# Registers an activity provider
-
1
def register(event_type, options={})
-
8
options.assert_valid_keys(:class_name, :default)
-
-
8
event_type = event_type.to_s
-
8
providers = options[:class_name] || event_type.classify
-
8
providers = ([] << providers) unless providers.is_a?(Array)
-
-
8
@@available_event_types << event_type unless @@available_event_types.include?(event_type)
-
8
@@default_event_types << event_type unless options[:default] == false
-
8
@@providers[event_type] += providers
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Ciphering
-
1
def self.included(base)
-
2
base.extend ClassMethods
-
end
-
-
1
class << self
-
1
def encrypt_text(text)
-
if cipher_key.blank? || text.blank?
-
text
-
else
-
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
-
iv = c.random_iv
-
c.encrypt
-
c.key = cipher_key
-
c.iv = iv
-
e = c.update(text.to_s)
-
e << c.final
-
"aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--')
-
end
-
end
-
-
1
def decrypt_text(text)
-
if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/)
-
if cipher_key.blank?
-
logger.error "Attempt to decrypt a ciphered text with no cipher key configured in config/configuration.yml" if logger
-
return text
-
end
-
text = match[1]
-
c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
-
e, iv = text.split("--").map {|s| Base64.decode64(s)}
-
c.decrypt
-
c.key = cipher_key
-
c.iv = iv
-
d = c.update(e)
-
d << c.final
-
else
-
text
-
end
-
end
-
-
1
def cipher_key
-
key = Redmine::Configuration['database_cipher_key'].to_s
-
key.blank? ? nil : Digest::SHA256.hexdigest(key)
-
end
-
-
1
def logger
-
Rails.logger
-
end
-
end
-
-
1
module ClassMethods
-
1
def encrypt_all(attribute)
-
transaction do
-
all.each do |object|
-
clear = object.send(attribute)
-
object.send "#{attribute}=", clear
-
raise(ActiveRecord::Rollback) unless object.save(:validation => false)
-
end
-
end ? true : false
-
end
-
-
1
def decrypt_all(attribute)
-
transaction do
-
all.each do |object|
-
clear = object.send(attribute)
-
object.send :write_attribute, attribute, clear
-
raise(ActiveRecord::Rollback) unless object.save(:validation => false)
-
end
-
end
-
end ? true : false
-
end
-
-
1
private
-
-
# Returns the value of the given ciphered attribute
-
1
def read_ciphered_attribute(attribute)
-
Redmine::Ciphering.decrypt_text(read_attribute(attribute))
-
end
-
-
# Sets the value of the given ciphered attribute
-
1
def write_ciphered_attribute(attribute, value)
-
write_attribute(attribute, Redmine::Ciphering.encrypt_text(value))
-
end
-
end
-
end
-
1
require 'iconv'
-
-
1
module Redmine
-
1
module CodesetUtil
-
-
1
def self.replace_invalid_utf8(str)
-
return str if str.nil?
-
if str.respond_to?(:force_encoding)
-
str.force_encoding('UTF-8')
-
if ! str.valid_encoding?
-
str = str.encode("US-ASCII", :invalid => :replace,
-
:undef => :replace, :replace => '?').encode("UTF-8")
-
end
-
elsif RUBY_PLATFORM == 'java'
-
begin
-
ic = Iconv.new('UTF-8', 'UTF-8')
-
str = ic.iconv(str)
-
rescue
-
str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
-
end
-
else
-
ic = Iconv.new('UTF-8', 'UTF-8')
-
txtar = ""
-
begin
-
txtar += ic.iconv(str)
-
rescue Iconv::IllegalSequence
-
txtar += $!.success
-
str = '?' + $!.failed[1,$!.failed.length]
-
retry
-
rescue
-
txtar += $!.success
-
end
-
str = txtar
-
end
-
str
-
end
-
-
1
def self.to_utf8(str, encoding)
-
return str if str.nil?
-
str.force_encoding("ASCII-8BIT") if str.respond_to?(:force_encoding)
-
if str.empty?
-
str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
-
return str
-
end
-
enc = encoding.blank? ? "UTF-8" : encoding
-
if str.respond_to?(:force_encoding)
-
if enc.upcase != "UTF-8"
-
str.force_encoding(enc)
-
str = str.encode("UTF-8", :invalid => :replace,
-
:undef => :replace, :replace => '?')
-
else
-
str.force_encoding("UTF-8")
-
if ! str.valid_encoding?
-
str = str.encode("US-ASCII", :invalid => :replace,
-
:undef => :replace, :replace => '?').encode("UTF-8")
-
end
-
end
-
elsif RUBY_PLATFORM == 'java'
-
begin
-
ic = Iconv.new('UTF-8', enc)
-
str = ic.iconv(str)
-
rescue
-
str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
-
end
-
else
-
ic = Iconv.new('UTF-8', enc)
-
txtar = ""
-
begin
-
txtar += ic.iconv(str)
-
rescue Iconv::IllegalSequence
-
txtar += $!.success
-
str = '?' + $!.failed[1,$!.failed.length]
-
retry
-
rescue
-
txtar += $!.success
-
end
-
str = txtar
-
end
-
str
-
end
-
-
1
def self.to_utf8_by_setting(str)
-
return str if str.nil?
-
str = self.to_utf8_by_setting_internal(str)
-
if str.respond_to?(:force_encoding)
-
str.force_encoding('UTF-8')
-
end
-
str
-
end
-
-
1
def self.to_utf8_by_setting_internal(str)
-
return str if str.nil?
-
if str.respond_to?(:force_encoding)
-
str.force_encoding('ASCII-8BIT')
-
end
-
return str if str.empty?
-
return str if /\A[\r\n\t\x20-\x7e]*\Z/n.match(str) # for us-ascii
-
if str.respond_to?(:force_encoding)
-
str.force_encoding('UTF-8')
-
end
-
encodings = Setting.repositories_encodings.split(',').collect(&:strip)
-
encodings.each do |encoding|
-
begin
-
return Iconv.conv('UTF-8', encoding, str)
-
rescue Iconv::Failure
-
# do nothing here and try the next encoding
-
end
-
end
-
str = self.replace_invalid_utf8(str)
-
if str.respond_to?(:force_encoding)
-
str.force_encoding('UTF-8')
-
end
-
str
-
end
-
-
1
def self.from_utf8(str, encoding)
-
str ||= ''
-
if str.respond_to?(:force_encoding)
-
str.force_encoding('UTF-8')
-
if encoding.upcase != 'UTF-8'
-
str = str.encode(encoding, :invalid => :replace,
-
:undef => :replace, :replace => '?')
-
else
-
str = self.replace_invalid_utf8(str)
-
end
-
elsif RUBY_PLATFORM == 'java'
-
begin
-
ic = Iconv.new(encoding, 'UTF-8')
-
str = ic.iconv(str)
-
rescue
-
str = str.gsub(%r{[^\r\n\t\x20-\x7e]}, '?')
-
end
-
else
-
ic = Iconv.new(encoding, 'UTF-8')
-
txtar = ""
-
begin
-
txtar += ic.iconv(str)
-
rescue Iconv::IllegalSequence
-
txtar += $!.success
-
str = '?' + $!.failed[1, $!.failed.length]
-
retry
-
rescue
-
txtar += $!.success
-
end
-
str = txtar
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Configuration
-
-
# Configuration default values
-
1
@defaults = {
-
'email_delivery' => nil
-
}
-
-
1
@config = nil
-
-
1
class << self
-
# Loads the Redmine configuration file
-
# Valid options:
-
# * <tt>:file</tt>: the configuration file to load (default: config/configuration.yml)
-
# * <tt>:env</tt>: the environment to load the configuration for (default: Rails.env)
-
1
def load(options={})
-
1
filename = options[:file] || File.join(Rails.root, 'config', 'configuration.yml')
-
1
env = options[:env] || Rails.env
-
-
1
@config = @defaults.dup
-
-
1
load_deprecated_email_configuration(env)
-
1
if File.file?(filename)
-
@config.merge!(load_from_yaml(filename, env))
-
end
-
-
# Compatibility mode for those who copy email.yml over configuration.yml
-
1
%w(delivery_method smtp_settings sendmail_settings).each do |key|
-
3
if value = @config.delete(key)
-
@config['email_delivery'] ||= {}
-
@config['email_delivery'][key] = value
-
end
-
end
-
-
1
if @config['email_delivery']
-
ActionMailer::Base.perform_deliveries = true
-
@config['email_delivery'].each do |k, v|
-
v.symbolize_keys! if v.respond_to?(:symbolize_keys!)
-
ActionMailer::Base.send("#{k}=", v)
-
end
-
end
-
-
1
@config
-
end
-
-
# Returns a configuration setting
-
1
def [](name)
-
9
load unless @config
-
9
@config[name]
-
end
-
-
# Yields a block with the specified hash configuration settings
-
1
def with(settings)
-
settings.stringify_keys!
-
load unless @config
-
was = settings.keys.inject({}) {|h,v| h[v] = @config[v]; h}
-
@config.merge! settings
-
yield if block_given?
-
@config.merge! was
-
end
-
-
1
private
-
-
1
def load_from_yaml(filename, env)
-
yaml = nil
-
begin
-
yaml = YAML::load_file(filename)
-
rescue ArgumentError
-
$stderr.puts "Your Redmine configuration file located at #{filename} is not a valid YAML file and could not be loaded."
-
exit 1
-
end
-
conf = {}
-
if yaml.is_a?(Hash)
-
if yaml['default']
-
conf.merge!(yaml['default'])
-
end
-
if yaml[env]
-
conf.merge!(yaml[env])
-
end
-
else
-
$stderr.puts "Your Redmine configuration file located at #{filename} is not a valid Redmine configuration file."
-
exit 1
-
end
-
conf
-
end
-
-
1
def load_deprecated_email_configuration(env)
-
1
deprecated_email_conf = File.join(Rails.root, 'config', 'email.yml')
-
1
if File.file?(deprecated_email_conf)
-
warn "Storing outgoing emails configuration in config/email.yml is deprecated. You should now store it in config/configuration.yml using the email_delivery setting."
-
@config.merge!({'email_delivery' => load_from_yaml(deprecated_email_conf, env)})
-
end
-
end
-
end
-
end
-
end
-
4
Dir[File.dirname(__FILE__) + "/core_ext/*.rb"].each { |file| require(file) }
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module ActiveRecord
-
1
module FinderMethods
-
1
def find_ids(*args)
-
find_ids_with_associations
-
end
-
-
1
private
-
-
1
def find_ids_with_associations
-
join_dependency = construct_join_dependency_for_association_find
-
relation = construct_relation_for_association_find_ids(join_dependency)
-
rows = connection.select_all(relation, 'SQL', relation.bind_values)
-
rows.map {|row| row["id"].to_i}
-
rescue ThrowResult
-
[]
-
end
-
-
1
def construct_relation_for_association_find_ids(join_dependency)
-
relation = except(:includes, :eager_load, :preload, :select).select("#{table_name}.id")
-
apply_join_dependency(relation, join_dependency)
-
end
-
end
-
end
-
1
require File.dirname(__FILE__) + '/date/calculations'
-
-
1
class Date #:nodoc:
-
1
include Redmine::CoreExtensions::Date::Calculations
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
1
module CoreExtensions #:nodoc:
-
1
module Date #:nodoc:
-
# Custom date calculations
-
1
module Calculations
-
# Returns difference with specified date in months
-
1
def months_ago(date = self.class.today)
-
(date.year - self.year)*12 + (date.month - self.month)
-
end
-
-
# Returns difference with specified date in weeks
-
1
def weeks_ago(date = self.class.today)
-
(date.year - self.year)*52 + (date.cweek - self.cweek)
-
end
-
end
-
end
-
end
-
end
-
1
require File.dirname(__FILE__) + '/string/conversions'
-
1
require File.dirname(__FILE__) + '/string/inflections'
-
-
1
class String #:nodoc:
-
1
include Redmine::CoreExtensions::String::Conversions
-
1
include Redmine::CoreExtensions::String::Inflections
-
-
1
def is_binary_data?
-
5243
( self.count( "^ -~", "^\r\n" ).fdiv(self.size) > 0.3 || self.index( "\x00" ) ) unless empty?
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
1
module CoreExtensions #:nodoc:
-
1
module String #:nodoc:
-
# Custom string conversions
-
1
module Conversions
-
# Parses hours format and returns a float
-
1
def to_hours
-
12
s = self.dup
-
12
s.strip!
-
12
if s =~ %r{^(\d+([.,]\d+)?)h?$}
-
10
s = $1
-
else
-
# 2:30 => 2.5
-
2
s.gsub!(%r{^(\d+):(\d+)$}) { $1.to_i + $2.to_i / 60.0 }
-
# 2h30, 2h, 30m => 2.5, 2, 0.5
-
4
s.gsub!(%r{^((\d+)\s*(h|hours?))?\s*((\d+)\s*(m|min)?)?$}i) { |m| ($1 || $4) ? ($2.to_i + $5.to_i / 60.0) : m[0] }
-
end
-
# 2,5 => 2.5
-
12
s.gsub!(',', '.')
-
14
begin; Kernel.Float(s); rescue; nil; end
-
end
-
-
# Object#to_a removed in ruby1.9
-
1
if RUBY_VERSION > '1.9'
-
1
def to_a
-
10172
[self.dup]
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
1
module CoreExtensions #:nodoc:
-
1
module String #:nodoc:
-
# Custom string inflections
-
1
module Inflections
-
1
def with_leading_slash
-
starts_with?('/') ? self : "/#{ self }"
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
class CustomFieldFormat
-
1
include Redmine::I18n
-
-
1
cattr_accessor :available
-
1
@@available = {}
-
-
1
attr_accessor :name, :order, :label, :edit_as, :class_names
-
-
1
def initialize(name, options={})
-
9
self.name = name
-
9
self.label = options[:label] || "label_#{name}".to_sym
-
9
self.order = options[:order] || self.class.available_formats.size
-
9
self.edit_as = options[:edit_as] || name
-
9
self.class_names = options[:only]
-
end
-
-
1
def format(value)
-
50
send "format_as_#{name}", value
-
end
-
-
1
def format_as_date(value)
-
begin; format_date(value.to_date); rescue; value end
-
end
-
-
1
def format_as_bool(value)
-
l(value == "1" ? :general_text_Yes : :general_text_No)
-
end
-
-
1
['string','text','int','float','list'].each do |name|
-
5
define_method("format_as_#{name}") {|value|
-
50
return value
-
}
-
end
-
-
1
['user', 'version'].each do |name|
-
2
define_method("format_as_#{name}") {|value|
-
return value.blank? ? "" : name.classify.constantize.find_by_id(value.to_i).to_s
-
}
-
end
-
-
1
class << self
-
1
def map(&block)
-
1
yield self
-
end
-
-
# Registers a custom field format
-
1
def register(*args)
-
9
custom_field_format = args.first
-
9
unless custom_field_format.is_a?(Redmine::CustomFieldFormat)
-
9
custom_field_format = Redmine::CustomFieldFormat.new(*args)
-
end
-
9
@@available[custom_field_format.name] = custom_field_format unless @@available.keys.include?(custom_field_format.name)
-
end
-
-
1
def available_formats
-
10
@@available.keys
-
end
-
-
1
def find_by_name(name)
-
54
@@available[name.to_s]
-
end
-
-
1
def label_for(name)
-
format = @@available[name.to_s]
-
format.label if format
-
end
-
-
# Return an array of custom field formats which can be used in select_tag
-
1
def as_select(class_name=nil)
-
fields = @@available.values
-
fields = fields.select {|field| field.class_names.nil? || field.class_names.include?(class_name)}
-
fields.sort {|a,b|
-
a.order <=> b.order
-
}.collect {|custom_field_format|
-
[ l(custom_field_format.label), custom_field_format.name ]
-
}
-
end
-
-
1
def format_value(value, field_format)
-
50
return "" unless value && !value.empty?
-
-
50
if format_type = find_by_name(field_format)
-
50
format_type.format(value)
-
else
-
value
-
end
-
end
-
end
-
end
-
end
-
# encoding: utf-8
-
#
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'iconv'
-
1
require 'fpdf/chinese'
-
1
require 'fpdf/japanese'
-
1
require 'fpdf/korean'
-
1
require 'core/rmagick'
-
-
1
module Redmine
-
1
module Export
-
1
module PDF
-
1
include ActionView::Helpers::TextHelper
-
1
include ActionView::Helpers::NumberHelper
-
1
include IssuesHelper
-
-
1
class ITCPDF < TCPDF
-
1
include Redmine::I18n
-
1
attr_accessor :footer_date
-
-
1
def initialize(lang)
-
@@k_path_cache = Rails.root.join('tmp', 'pdf')
-
FileUtils.mkdir_p @@k_path_cache unless File::exist?(@@k_path_cache)
-
set_language_if_valid lang
-
pdf_encoding = l(:general_pdf_encoding).upcase
-
super('P', 'mm', 'A4', (pdf_encoding == 'UTF-8'), pdf_encoding)
-
case current_language.to_s.downcase
-
when 'vi'
-
@font_for_content = 'DejaVuSans'
-
@font_for_footer = 'DejaVuSans'
-
else
-
case pdf_encoding
-
when 'UTF-8'
-
@font_for_content = 'FreeSans'
-
@font_for_footer = 'FreeSans'
-
when 'CP949'
-
extend(PDF_Korean)
-
AddUHCFont()
-
@font_for_content = 'UHC'
-
@font_for_footer = 'UHC'
-
when 'CP932', 'SJIS', 'SHIFT_JIS'
-
extend(PDF_Japanese)
-
AddSJISFont()
-
@font_for_content = 'SJIS'
-
@font_for_footer = 'SJIS'
-
when 'GB18030'
-
extend(PDF_Chinese)
-
AddGBFont()
-
@font_for_content = 'GB'
-
@font_for_footer = 'GB'
-
when 'BIG5'
-
extend(PDF_Chinese)
-
AddBig5Font()
-
@font_for_content = 'Big5'
-
@font_for_footer = 'Big5'
-
else
-
@font_for_content = 'Arial'
-
@font_for_footer = 'Helvetica'
-
end
-
end
-
SetCreator(Redmine::Info.app_name)
-
SetFont(@font_for_content)
-
@outlines = []
-
@outlineRoot = nil
-
end
-
-
1
def SetFontStyle(style, size)
-
SetFont(@font_for_content, style, size)
-
end
-
-
1
def SetTitle(txt)
-
txt = begin
-
utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
-
hextxt = "<FEFF" # FEFF is BOM
-
hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
-
hextxt << ">"
-
rescue
-
txt
-
end || ''
-
super(txt)
-
end
-
-
1
def textstring(s)
-
# Format a text string
-
if s =~ /^</ # This means the string is hex-dumped.
-
return s
-
else
-
return '('+escape(s)+')'
-
end
-
end
-
-
1
def fix_text_encoding(txt)
-
RDMPdfEncoding::rdm_from_utf8(txt, l(:general_pdf_encoding))
-
end
-
-
1
def RDMCell(w ,h=0, txt='', border=0, ln=0, align='', fill=0, link='')
-
Cell(w, h, fix_text_encoding(txt), border, ln, align, fill, link)
-
end
-
-
1
def RDMMultiCell(w, h=0, txt='', border=0, align='', fill=0, ln=1)
-
MultiCell(w, h, fix_text_encoding(txt), border, align, fill, ln)
-
end
-
-
1
def RDMwriteHTMLCell(w, h, x, y, txt='', attachments=[], border=0, ln=1, fill=0)
-
@attachments = attachments
-
writeHTMLCell(w, h, x, y,
-
fix_text_encoding(
-
Redmine::WikiFormatting.to_html(Setting.text_formatting, txt)),
-
border, ln, fill)
-
end
-
-
1
def getImageFilename(attrname)
-
# attrname: general_pdf_encoding string file/uri name
-
atta = RDMPdfEncoding.attach(@attachments, attrname, l(:general_pdf_encoding))
-
if atta
-
return atta.diskfile
-
else
-
return nil
-
end
-
end
-
-
1
def Footer
-
SetFont(@font_for_footer, 'I', 8)
-
SetY(-15)
-
SetX(15)
-
RDMCell(0, 5, @footer_date, 0, 0, 'L')
-
SetY(-15)
-
SetX(-30)
-
RDMCell(0, 5, PageNo().to_s + '/{nb}', 0, 0, 'C')
-
end
-
-
1
def Bookmark(txt, level=0, y=0)
-
if (y == -1)
-
y = GetY()
-
end
-
@outlines << {:t => txt, :l => level, :p => PageNo(), :y => (@h - y)*@k}
-
end
-
-
1
def bookmark_title(txt)
-
txt = begin
-
utf16txt = Iconv.conv('UTF-16BE', 'UTF-8', txt)
-
hextxt = "<FEFF" # FEFF is BOM
-
hextxt << utf16txt.unpack("C*").map {|x| sprintf("%02X",x) }.join
-
hextxt << ">"
-
rescue
-
txt
-
end || ''
-
end
-
-
1
def putbookmarks
-
nb=@outlines.size
-
return if (nb==0)
-
lru=[]
-
level=0
-
@outlines.each_with_index do |o, i|
-
if(o[:l]>0)
-
parent=lru[o[:l]-1]
-
#Set parent and last pointers
-
@outlines[i][:parent]=parent
-
@outlines[parent][:last]=i
-
if (o[:l]>level)
-
#Level increasing: set first pointer
-
@outlines[parent][:first]=i
-
end
-
else
-
@outlines[i][:parent]=nb
-
end
-
if (o[:l]<=level && i>0)
-
#Set prev and next pointers
-
prev=lru[o[:l]]
-
@outlines[prev][:next]=i
-
@outlines[i][:prev]=prev
-
end
-
lru[o[:l]]=i
-
level=o[:l]
-
end
-
#Outline items
-
n=self.n+1
-
@outlines.each_with_index do |o, i|
-
newobj()
-
out('<</Title '+bookmark_title(o[:t]))
-
out("/Parent #{n+o[:parent]} 0 R")
-
if (o[:prev])
-
out("/Prev #{n+o[:prev]} 0 R")
-
end
-
if (o[:next])
-
out("/Next #{n+o[:next]} 0 R")
-
end
-
if (o[:first])
-
out("/First #{n+o[:first]} 0 R")
-
end
-
if (o[:last])
-
out("/Last #{n+o[:last]} 0 R")
-
end
-
out("/Dest [%d 0 R /XYZ 0 %.2f null]" % [1+2*o[:p], o[:y]])
-
out('/Count 0>>')
-
out('endobj')
-
end
-
#Outline root
-
newobj()
-
@outlineRoot=self.n
-
out("<</Type /Outlines /First #{n} 0 R");
-
out("/Last #{n+lru[0]} 0 R>>");
-
out('endobj');
-
end
-
-
1
def putresources()
-
super
-
putbookmarks()
-
end
-
-
1
def putcatalog()
-
super
-
if(@outlines.size > 0)
-
out("/Outlines #{@outlineRoot} 0 R");
-
out('/PageMode /UseOutlines');
-
end
-
end
-
end
-
-
# fetch row values
-
1
def fetch_row_values(issue, query, level)
-
query.columns.collect do |column|
-
s = if column.is_a?(QueryCustomFieldColumn)
-
cv = issue.custom_field_values.detect {|v| v.custom_field_id == column.custom_field.id}
-
show_value(cv)
-
else
-
value = issue.send(column.name)
-
if column.name == :subject
-
value = " " * level + value
-
end
-
if value.is_a?(Date)
-
format_date(value)
-
elsif value.is_a?(Time)
-
format_time(value)
-
else
-
value
-
end
-
end
-
s.to_s
-
end
-
end
-
-
# calculate columns width
-
1
def calc_col_width(issues, query, table_width, pdf)
-
# calculate statistics
-
# by captions
-
pdf.SetFontStyle('B',8)
-
col_padding = pdf.GetStringWidth('OO')
-
col_width_min = query.columns.map {|v| pdf.GetStringWidth(v.caption) + col_padding}
-
col_width_max = Array.new(col_width_min)
-
col_width_avg = Array.new(col_width_min)
-
word_width_max = query.columns.map {|c|
-
n = 10
-
c.caption.split.each {|w|
-
x = pdf.GetStringWidth(w) + col_padding
-
n = x if n < x
-
}
-
n
-
}
-
-
# by properties of issues
-
pdf.SetFontStyle('',8)
-
col_padding = pdf.GetStringWidth('OO')
-
k = 1
-
issue_list(issues) {|issue, level|
-
k += 1
-
values = fetch_row_values(issue, query, level)
-
values.each_with_index {|v,i|
-
n = pdf.GetStringWidth(v) + col_padding
-
col_width_max[i] = n if col_width_max[i] < n
-
col_width_min[i] = n if col_width_min[i] > n
-
col_width_avg[i] += n
-
v.split.each {|w|
-
x = pdf.GetStringWidth(w) + col_padding
-
word_width_max[i] = x if word_width_max[i] < x
-
}
-
}
-
}
-
col_width_avg.map! {|x| x / k}
-
-
# calculate columns width
-
ratio = table_width / col_width_avg.inject(0) {|s,w| s += w}
-
col_width = col_width_avg.map {|w| w * ratio}
-
-
# correct max word width if too many columns
-
ratio = table_width / word_width_max.inject(0) {|s,w| s += w}
-
word_width_max.map! {|v| v * ratio} if ratio < 1
-
-
# correct and lock width of some columns
-
done = 1
-
col_fix = []
-
col_width.each_with_index do |w,i|
-
if w > col_width_max[i]
-
col_width[i] = col_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
elsif w < word_width_max[i]
-
col_width[i] = word_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
else
-
col_fix[i] = 0
-
end
-
end
-
-
# iterate while need to correct and lock coluns width
-
while done == 0
-
# calculate free & locked columns width
-
done = 1
-
fix_col_width = 0
-
free_col_width = 0
-
col_width.each_with_index do |w,i|
-
if col_fix[i] == 1
-
fix_col_width += w
-
else
-
free_col_width += w
-
end
-
end
-
-
# calculate column normalizing ratio
-
if free_col_width == 0
-
ratio = table_width / col_width.inject(0) {|s,w| s += w}
-
else
-
ratio = (table_width - fix_col_width) / free_col_width
-
end
-
-
# correct columns width
-
col_width.each_with_index do |w,i|
-
if col_fix[i] == 0
-
col_width[i] = w * ratio
-
-
# check if column width less then max word width
-
if col_width[i] < word_width_max[i]
-
col_width[i] = word_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
elsif col_width[i] > col_width_max[i]
-
col_width[i] = col_width_max[i]
-
col_fix[i] = 1
-
done = 0
-
end
-
end
-
end
-
end
-
col_width
-
end
-
-
1
def render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
-
# headers
-
pdf.SetFontStyle('B',8)
-
pdf.SetFillColor(230, 230, 230)
-
-
# render it background to find the max height used
-
base_x = pdf.GetX
-
base_y = pdf.GetY
-
max_height = issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
-
pdf.Rect(base_x, base_y, table_width + col_id_width, max_height, 'FD');
-
pdf.SetXY(base_x, base_y);
-
-
# write the cells on page
-
pdf.RDMCell(col_id_width, row_height, "#", "T", 0, 'C', 1)
-
issues_to_pdf_write_cells(pdf, query.columns, col_width, row_height, true)
-
issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
-
pdf.SetY(base_y + max_height);
-
-
# rows
-
pdf.SetFontStyle('',8)
-
pdf.SetFillColor(255, 255, 255)
-
end
-
-
# Returns a PDF string of a list of issues
-
1
def issues_to_pdf(issues, project, query)
-
pdf = ITCPDF.new(current_language)
-
title = query.new_record? ? l(:label_issue_plural) : query.name
-
title = "#{project} - #{title}" if project
-
pdf.SetTitle(title)
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.SetAutoPageBreak(false)
-
pdf.AddPage("L")
-
-
# Landscape A4 = 210 x 297 mm
-
page_height = 210
-
page_width = 297
-
right_margin = 10
-
bottom_margin = 20
-
col_id_width = 10
-
row_height = 4
-
-
# column widths
-
table_width = page_width - right_margin - 10 # fixed left margin
-
col_width = []
-
unless query.columns.empty?
-
col_width = calc_col_width(issues, query, table_width - col_id_width, pdf)
-
table_width = col_width.inject(0) {|s,v| s += v}
-
end
-
-
# title
-
pdf.SetFontStyle('B',11)
-
pdf.RDMCell(190,10, title)
-
pdf.Ln
-
render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
-
previous_group = false
-
issue_list(issues) do |issue, level|
-
if query.grouped? &&
-
(group = query.group_by_column.value(issue)) != previous_group
-
pdf.SetFontStyle('B',10)
-
group_label = group.blank? ? 'None' : group.to_s
-
group_label << " (#{query.issue_count_by_group[group]})"
-
pdf.Bookmark group_label, 0, -1
-
pdf.RDMCell(table_width + col_id_width, row_height * 2, group_label, 1, 1, 'L')
-
pdf.SetFontStyle('',8)
-
previous_group = group
-
end
-
-
# fetch row values
-
col_values = fetch_row_values(issue, query, level)
-
-
# render it off-page to find the max height used
-
base_x = pdf.GetX
-
base_y = pdf.GetY
-
pdf.SetY(2 * page_height)
-
max_height = issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
-
pdf.SetXY(base_x, base_y)
-
-
# make new page if it doesn't fit on the current one
-
space_left = page_height - base_y - bottom_margin
-
if max_height > space_left
-
pdf.AddPage("L")
-
render_table_header(pdf, query, col_width, row_height, col_id_width, table_width)
-
base_x = pdf.GetX
-
base_y = pdf.GetY
-
end
-
-
# write the cells on page
-
pdf.RDMCell(col_id_width, row_height, issue.id.to_s, "T", 0, 'C', 1)
-
issues_to_pdf_write_cells(pdf, col_values, col_width, row_height)
-
issues_to_pdf_draw_borders(pdf, base_x, base_y, base_y + max_height, col_id_width, col_width)
-
pdf.SetY(base_y + max_height);
-
end
-
-
if issues.size == Setting.issues_export_limit.to_i
-
pdf.SetFontStyle('B',10)
-
pdf.RDMCell(0, row_height, '...')
-
end
-
pdf.Output
-
end
-
-
# Renders MultiCells and returns the maximum height used
-
1
def issues_to_pdf_write_cells(pdf, col_values, col_widths,
-
row_height, head=false)
-
base_y = pdf.GetY
-
max_height = row_height
-
col_values.each_with_index do |column, i|
-
col_x = pdf.GetX
-
if head == true
-
pdf.RDMMultiCell(col_widths[i], row_height, column.caption, "T", 'L', 1)
-
else
-
pdf.RDMMultiCell(col_widths[i], row_height, column, "T", 'L', 1)
-
end
-
max_height = (pdf.GetY - base_y) if (pdf.GetY - base_y) > max_height
-
pdf.SetXY(col_x + col_widths[i], base_y);
-
end
-
return max_height
-
end
-
-
# Draw lines to close the row (MultiCell border drawing in not uniform)
-
1
def issues_to_pdf_draw_borders(pdf, top_x, top_y, lower_y,
-
id_width, col_widths)
-
col_x = top_x + id_width
-
pdf.Line(col_x, top_y, col_x, lower_y) # id right border
-
col_widths.each do |width|
-
col_x += width
-
pdf.Line(col_x, top_y, col_x, lower_y) # columns right border
-
end
-
pdf.Line(top_x, top_y, top_x, lower_y) # left border
-
pdf.Line(top_x, lower_y, col_x, lower_y) # bottom border
-
end
-
-
# Returns a PDF string of a single issue
-
1
def issue_to_pdf(issue)
-
pdf = ITCPDF.new(current_language)
-
pdf.SetTitle("#{issue.project} - ##{issue.tracker} #{issue.id}")
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.AddPage
-
pdf.SetFontStyle('B',11)
-
buf = "#{issue.project} - #{issue.tracker} # #{issue.id}"
-
pdf.RDMMultiCell(190, 5, buf)
-
pdf.Ln
-
pdf.SetFontStyle('',8)
-
base_x = pdf.GetX
-
i = 1
-
issue.ancestors.each do |ancestor|
-
pdf.SetX(base_x + i)
-
buf = "#{ancestor.tracker} # #{ancestor.id} (#{ancestor.status.to_s}): #{ancestor.subject}"
-
pdf.RDMMultiCell(190 - i, 5, buf)
-
i += 1 if i < 35
-
end
-
pdf.Ln
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_status) + ":","LT")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, issue.status.to_s,"RT")
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_priority) + ":","LT")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, issue.priority.to_s,"RT")
-
pdf.Ln
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_author) + ":","L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, issue.author.to_s,"R")
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_category) + ":","L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, issue.category.to_s,"R")
-
pdf.Ln
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_created_on) + ":","L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, format_date(issue.created_on),"R")
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_assigned_to) + ":","L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, issue.assigned_to.to_s,"R")
-
pdf.Ln
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_updated_on) + ":","LB")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, format_date(issue.updated_on),"RB")
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_due_date) + ":","LB")
-
pdf.SetFontStyle('',9)
-
pdf.RDMCell(60,5, format_date(issue.due_date),"RB")
-
pdf.Ln
-
-
for custom_value in issue.custom_field_values
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, custom_value.custom_field.name + ":","L")
-
pdf.SetFontStyle('',9)
-
pdf.RDMMultiCell(155,5, (show_value custom_value),"R")
-
end
-
-
y0 = pdf.GetY
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35,5, l(:field_subject) + ":","LT")
-
pdf.SetFontStyle('',9)
-
pdf.RDMMultiCell(155,5, issue.subject,"RT")
-
pdf.Line(pdf.GetX, y0, pdf.GetX, pdf.GetY)
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35+155, 5, l(:field_description), "LRT", 1)
-
pdf.SetFontStyle('',9)
-
-
# Set resize image scale
-
pdf.SetImageScale(1.6)
-
pdf.RDMwriteHTMLCell(35+155, 5, 0, 0,
-
issue.description.to_s, issue.attachments, "LRB")
-
-
unless issue.leaf?
-
# for CJK
-
truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 90 : 65 )
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35+155,5, l(:label_subtask_plural) + ":", "LTR")
-
pdf.Ln
-
issue_list(issue.descendants.sort_by(&:lft)) do |child, level|
-
buf = truncate("#{child.tracker} # #{child.id}: #{child.subject}",
-
:length => truncate_length)
-
level = 10 if level >= 10
-
pdf.SetFontStyle('',8)
-
pdf.RDMCell(35+135,5, (level >=1 ? " " * level : "") + buf, "L")
-
pdf.SetFontStyle('B',8)
-
pdf.RDMCell(20,5, child.status.to_s, "R")
-
pdf.Ln
-
end
-
end
-
-
relations = issue.relations.select { |r| r.other_issue(issue).visible? }
-
unless relations.empty?
-
# for CJK
-
truncate_length = ( l(:general_pdf_encoding).upcase == "UTF-8" ? 80 : 60 )
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(35+155,5, l(:label_related_issues) + ":", "LTR")
-
pdf.Ln
-
relations.each do |relation|
-
buf = ""
-
buf += "#{l(relation.label_for(issue))} "
-
if relation.delay && relation.delay != 0
-
buf += "(#{l('datetime.distance_in_words.x_days', :count => relation.delay)}) "
-
end
-
if Setting.cross_project_issue_relations?
-
buf += "#{relation.other_issue(issue).project} - "
-
end
-
buf += "#{relation.other_issue(issue).tracker}" +
-
" # #{relation.other_issue(issue).id}: #{relation.other_issue(issue).subject}"
-
buf = truncate(buf, :length => truncate_length)
-
pdf.SetFontStyle('', 8)
-
pdf.RDMCell(35+155-60, 5, buf, "L")
-
pdf.SetFontStyle('B',8)
-
pdf.RDMCell(20,5, relation.other_issue(issue).status.to_s, "")
-
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).start_date), "")
-
pdf.RDMCell(20,5, format_date(relation.other_issue(issue).due_date), "R")
-
pdf.Ln
-
end
-
end
-
pdf.RDMCell(190,5, "", "T")
-
pdf.Ln
-
-
if issue.changesets.any? &&
-
User.current.allowed_to?(:view_changesets, issue.project)
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_associated_revisions), "B")
-
pdf.Ln
-
for changeset in issue.changesets
-
pdf.SetFontStyle('B',8)
-
csstr = "#{l(:label_revision)} #{changeset.format_identifier} - "
-
csstr += format_time(changeset.committed_on) + " - " + changeset.author.to_s
-
pdf.RDMCell(190, 5, csstr)
-
pdf.Ln
-
unless changeset.comments.blank?
-
pdf.SetFontStyle('',8)
-
pdf.RDMwriteHTMLCell(190,5,0,0,
-
changeset.comments.to_s, issue.attachments, "")
-
end
-
pdf.Ln
-
end
-
end
-
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_history), "B")
-
pdf.Ln
-
indice = 0
-
for journal in issue.journals.find(
-
:all, :include => [:user, :details],
-
:order => "#{Journal.table_name}.created_on ASC")
-
indice = indice + 1
-
pdf.SetFontStyle('B',8)
-
pdf.RDMCell(190,5,
-
"#" + indice.to_s +
-
" - " + format_time(journal.created_on) +
-
" - " + journal.user.name)
-
pdf.Ln
-
pdf.SetFontStyle('I',8)
-
details_to_strings(journal.details, true).each do |string|
-
pdf.RDMMultiCell(190,5, "- " + string)
-
end
-
if journal.notes?
-
pdf.Ln unless journal.details.empty?
-
pdf.SetFontStyle('',8)
-
pdf.RDMwriteHTMLCell(190,5,0,0,
-
journal.notes.to_s, issue.attachments, "")
-
end
-
pdf.Ln
-
end
-
-
if issue.attachments.any?
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
-
pdf.Ln
-
for attachment in issue.attachments
-
pdf.SetFontStyle('',8)
-
pdf.RDMCell(80,5, attachment.filename)
-
pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
-
pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
-
pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
-
pdf.Ln
-
end
-
end
-
pdf.Output
-
end
-
-
# Returns a PDF string of a set of wiki pages
-
1
def wiki_pages_to_pdf(pages, project)
-
pdf = ITCPDF.new(current_language)
-
pdf.SetTitle(project.name)
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.AddPage
-
pdf.SetFontStyle('B',11)
-
pdf.RDMMultiCell(190,5, project.name)
-
pdf.Ln
-
# Set resize image scale
-
pdf.SetImageScale(1.6)
-
pdf.SetFontStyle('',9)
-
write_page_hierarchy(pdf, pages.group_by(&:parent_id))
-
pdf.Output
-
end
-
-
# Returns a PDF string of a single wiki page
-
1
def wiki_page_to_pdf(page, project)
-
pdf = ITCPDF.new(current_language)
-
pdf.SetTitle("#{project} - #{page.title}")
-
pdf.alias_nb_pages
-
pdf.footer_date = format_date(Date.today)
-
pdf.AddPage
-
pdf.SetFontStyle('B',11)
-
pdf.RDMMultiCell(190,5,
-
"#{project} - #{page.title} - # #{page.content.version}")
-
pdf.Ln
-
# Set resize image scale
-
pdf.SetImageScale(1.6)
-
pdf.SetFontStyle('',9)
-
write_wiki_page(pdf, page)
-
pdf.Output
-
end
-
-
1
def write_page_hierarchy(pdf, pages, node=nil, level=0)
-
if pages[node]
-
pages[node].each do |page|
-
if @new_page
-
pdf.AddPage
-
else
-
@new_page = true
-
end
-
pdf.Bookmark page.title, level
-
write_wiki_page(pdf, page)
-
write_page_hierarchy(pdf, pages, page.id, level + 1) if pages[page.id]
-
end
-
end
-
end
-
-
1
def write_wiki_page(pdf, page)
-
pdf.RDMwriteHTMLCell(190,5,0,0,
-
page.content.text.to_s, page.attachments, 0)
-
if page.attachments.any?
-
pdf.Ln
-
pdf.SetFontStyle('B',9)
-
pdf.RDMCell(190,5, l(:label_attachment_plural), "B")
-
pdf.Ln
-
for attachment in page.attachments
-
pdf.SetFontStyle('',8)
-
pdf.RDMCell(80,5, attachment.filename)
-
pdf.RDMCell(20,5, number_to_human_size(attachment.filesize),0,0,"R")
-
pdf.RDMCell(25,5, format_date(attachment.created_on),0,0,"R")
-
pdf.RDMCell(65,5, attachment.author.name,0,0,"R")
-
pdf.Ln
-
end
-
end
-
end
-
-
1
class RDMPdfEncoding
-
1
def self.rdm_from_utf8(txt, encoding)
-
txt ||= ''
-
txt = Redmine::CodesetUtil.from_utf8(txt, encoding)
-
if txt.respond_to?(:force_encoding)
-
txt.force_encoding('ASCII-8BIT')
-
end
-
txt
-
end
-
-
1
def self.attach(attachments, filename, encoding)
-
filename_utf8 = Redmine::CodesetUtil.to_utf8(filename, encoding)
-
atta = nil
-
if filename_utf8 =~ /^[^\/"]+\.(gif|jpg|jpe|jpeg|png)$/i
-
atta = Attachment.latest_attach(attachments, filename_utf8)
-
end
-
if atta && atta.readable? && atta.visible?
-
return atta
-
else
-
return nil
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Helpers
-
1
class Diff
-
1
include ERB::Util
-
1
include ActionView::Helpers::TagHelper
-
1
include ActionView::Helpers::TextHelper
-
1
attr_reader :diff, :words
-
-
1
def initialize(content_to, content_from)
-
@words = content_to.to_s.split(/(\s+)/)
-
@words = @words.select {|word| word != ' '}
-
words_from = content_from.to_s.split(/(\s+)/)
-
words_from = words_from.select {|word| word != ' '}
-
@diff = words_from.diff @words
-
end
-
-
1
def to_html
-
words = self.words.collect{|word| h(word)}
-
words_add = 0
-
words_del = 0
-
dels = 0
-
del_off = 0
-
diff.diffs.each do |diff|
-
add_at = nil
-
add_to = nil
-
del_at = nil
-
deleted = ""
-
diff.each do |change|
-
pos = change[1]
-
if change[0] == "+"
-
add_at = pos + dels unless add_at
-
add_to = pos + dels
-
words_add += 1
-
else
-
del_at = pos unless del_at
-
deleted << ' ' + h(change[2])
-
words_del += 1
-
end
-
end
-
if add_at
-
words[add_at] = '<span class="diff_in">' + words[add_at]
-
words[add_to] = words[add_to] + '</span>'
-
end
-
if del_at
-
words.insert del_at - del_off + dels + words_add, '<span class="diff_out">' + deleted + '</span>'
-
dels += 1
-
del_off += words_del
-
words_del = 0
-
end
-
end
-
words.join(' ').html_safe
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Hook
-
1
@@listener_classes = []
-
1
@@listeners = nil
-
1
@@hook_listeners = {}
-
-
1
class << self
-
# Adds a listener class.
-
# Automatically called when a class inherits from Redmine::Hook::Listener.
-
1
def add_listener(klass)
-
2
raise "Hooks must include Singleton module." unless klass.included_modules.include?(Singleton)
-
2
@@listener_classes << klass
-
2
clear_listeners_instances
-
end
-
-
# Returns all the listerners instances.
-
1
def listeners
-
30
@@listeners ||= @@listener_classes.collect {|listener| listener.instance}
-
end
-
-
# Returns the listeners instances for the given hook.
-
1
def hook_listeners(hook)
-
2593
@@hook_listeners[hook] ||= listeners.select {|listener| listener.respond_to?(hook)}
-
end
-
-
# Clears all the listeners.
-
1
def clear_listeners
-
@@listener_classes = []
-
clear_listeners_instances
-
end
-
-
# Clears all the listeners instances.
-
1
def clear_listeners_instances
-
2
@@listeners = nil
-
2
@@hook_listeners = {}
-
end
-
-
# Calls a hook.
-
# Returns the listeners response.
-
1
def call_hook(hook, context={})
-
2537
[].tap do |response|
-
2537
hls = hook_listeners(hook)
-
2537
if hls.any?
-
988
hls.each {|listener| response << listener.send(hook, context)}
-
end
-
end
-
end
-
end
-
-
# Base class for hook listeners.
-
1
class Listener
-
1
include Singleton
-
1
include Redmine::I18n
-
-
# Registers the listener
-
1
def self.inherited(child)
-
2
Redmine::Hook.add_listener(child)
-
2
super
-
end
-
-
end
-
-
# Listener class used for views hooks.
-
# Listeners that inherit this class will include various helpers by default.
-
1
class ViewListener < Listener
-
1
include ERB::Util
-
1
include ActionView::Helpers::TagHelper
-
1
include ActionView::Helpers::FormHelper
-
1
include ActionView::Helpers::FormTagHelper
-
1
include ActionView::Helpers::FormOptionsHelper
-
1
include ActionView::Helpers::JavaScriptHelper
-
1
include ActionView::Helpers::NumberHelper
-
1
include ActionView::Helpers::UrlHelper
-
1
include ActionView::Helpers::AssetTagHelper
-
1
include ActionView::Helpers::TextHelper
-
1
include Rails.application.routes.url_helpers
-
1
include ApplicationHelper
-
-
# Default to creating links using only the path. Subclasses can
-
# change this default as needed
-
1
def self.default_url_options
-
16
{:only_path => true }
-
end
-
-
# Helper method to directly render a partial using the context:
-
#
-
# class MyHook < Redmine::Hook::ViewListener
-
# render_on :view_issues_show_details_bottom, :partial => "show_more_data"
-
# end
-
#
-
1
def self.render_on(hook, options={})
-
define_method hook do |context|
-
context[:controller].send(:render_to_string, {:locals => context}.merge(options))
-
end
-
end
-
-
1
def controller
-
nil
-
end
-
-
1
def config
-
2
ActionController::Base.config
-
end
-
end
-
-
# Helper module included in ApplicationHelper and ActionController so that
-
# hooks can be called in views like this:
-
#
-
# <%= call_hook(:some_hook) %>
-
# <%= call_hook(:another_hook, :foo => 'bar') %>
-
#
-
# Or in controllers like:
-
# call_hook(:some_hook)
-
# call_hook(:another_hook, :foo => 'bar')
-
#
-
# Hooks added to views will be concatenated into a string. Hooks added to
-
# controllers will return an array of results.
-
#
-
# Several objects are automatically added to the call context:
-
#
-
# * project => current project
-
# * request => Request instance
-
# * controller => current Controller instance
-
#
-
1
module Helper
-
1
def call_hook(hook, context={})
-
2537
if is_a?(ActionController::Base)
-
129
default_context = {:controller => self, :project => @project, :request => request}
-
129
Redmine::Hook.call_hook(hook, default_context.merge(context))
-
else
-
2408
default_context = { :project => @project }
-
2408
default_context[:controller] = controller if respond_to?(:controller)
-
2408
default_context[:request] = request if respond_to?(:request)
-
2408
Redmine::Hook.call_hook(hook, default_context.merge(context)).join(' ').html_safe
-
end
-
end
-
end
-
end
-
end
-
-
1
ApplicationHelper.send(:include, Redmine::Hook::Helper)
-
1
ActionController::Base.send(:include, Redmine::Hook::Helper)
-
1
module Redmine
-
1
module I18n
-
1
def self.included(base)
-
13
base.extend Redmine::I18n
-
end
-
-
1
def l(*args)
-
36038
case args.size
-
when 1
-
32618
::I18n.t(*args)
-
when 2
-
3420
if args.last.is_a?(Hash)
-
3278
::I18n.t(*args)
-
142
elsif args.last.is_a?(String)
-
134
::I18n.t(args.first, :value => args.last)
-
else
-
8
::I18n.t(args.first, :count => args.last)
-
end
-
else
-
raise "Translation string with multiple values: #{args.first}"
-
end
-
end
-
-
1
def l_or_humanize(s, options={})
-
2287
k = "#{options[:prefix]}#{s}".to_sym
-
2287
::I18n.t(k, :default => s.to_s.humanize)
-
end
-
-
1
def l_hours(hours)
-
71
hours = hours.to_f
-
71
l((hours < 2.0 ? :label_f_hour : :label_f_hour_plural), :value => ("%.2f" % hours.to_f))
-
end
-
-
1
def ll(lang, str, value=nil)
-
::I18n.t(str.to_s, :value => value, :locale => lang.to_s.gsub(%r{(.+)\-(.+)$}) { "#{$1}-#{$2.upcase}" })
-
end
-
-
1
def format_date(date)
-
252
return nil unless date
-
252
Setting.date_format.blank? ? ::I18n.l(date.to_date) : date.strftime(Setting.date_format)
-
end
-
-
1
def format_time(time, include_date = true)
-
208
return nil unless time
-
208
time = time.to_time if time.is_a?(String)
-
208
zone = User.current.time_zone
-
208
local = zone ? time.in_time_zone(zone) : (time.utc? ? time.localtime : time)
-
208
(include_date ? "#{format_date(local)} " : "") +
-
416
(Setting.time_format.blank? ? ::I18n.l(local, :format => :time) : local.strftime(Setting.time_format))
-
end
-
-
1
def day_name(day)
-
::I18n.t('date.day_names')[day % 7]
-
end
-
-
1
def month_name(month)
-
::I18n.t('date.month_names')[month]
-
end
-
-
1
def valid_languages
-
3855
@@valid_languages ||= Dir.glob(File.join(Rails.root, 'config', 'locales', '*.yml')).collect {|f| File.basename(f).split('.').first}.collect(&:to_sym)
-
end
-
-
1
def find_language(lang)
-
182784
@@languages_lookup = valid_languages.inject({}) {|k, v| k[v.to_s.downcase] = v; k }
-
3808
@@languages_lookup[lang.to_s.downcase]
-
end
-
-
1
def set_language_if_valid(lang)
-
3080
if l = find_language(lang)
-
3080
::I18n.locale = l
-
end
-
end
-
-
1
def current_language
-
1143
::I18n.locale
-
end
-
end
-
end
-
1
module Redmine
-
1
module Info
-
1
class << self
-
719
def app_name; 'Redmine' end
-
360
def url; 'http://www.redmine.org/' end
-
2
def help_url; 'http://www.redmine.org/guide' end
-
1
def versioned_name; "#{app_name} #{Redmine::VERSION}" end
-
-
1
def environment
-
s = "Environment:\n"
-
s << [
-
["Redmine version", Redmine::VERSION],
-
["Ruby version", "#{RUBY_VERSION} (#{RUBY_PLATFORM})"],
-
["Rails version", Rails::VERSION::STRING],
-
["Environment", Rails.env],
-
["Database adapter", ActiveRecord::Base.connection.adapter_name]
-
].map {|info| " %-40s %s" % info}.join("\n")
-
s << "\nRedmine plugins:\n"
-
-
plugins = Redmine::Plugin.all
-
if plugins.any?
-
s << plugins.map {|plugin| " %-40s %s" % [plugin.id.to_s, plugin.version.to_s]}.join("\n")
-
else
-
s << " no plugin installed"
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module MenuManager
-
1
class MenuError < StandardError #:nodoc:
-
end
-
-
1
module MenuController
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
28
@@menu_items = Hash.new {|hash, key| hash[key] = {:default => key, :actions => {}}}
-
1
mattr_accessor :menu_items
-
-
# Set the menu item name for a controller or specific actions
-
# Examples:
-
# * menu_item :tickets # => sets the menu name to :tickets for the whole controller
-
# * menu_item :tickets, :only => :list # => sets the menu name to :tickets for the 'list' action only
-
# * menu_item :tickets, :only => [:list, :show] # => sets the menu name to :tickets for 2 actions only
-
#
-
# The default menu item name for a controller is controller_name by default
-
# Eg. the default menu item name for ProjectsController is :projects
-
1
def menu_item(id, options = {})
-
23
if actions = options[:only]
-
8
actions = [] << actions unless actions.is_a?(Array)
-
22
actions.each {|a| menu_items[controller_name.to_sym][:actions][a.to_sym] = id}
-
else
-
15
menu_items[controller_name.to_sym][:default] = id
-
end
-
end
-
end
-
-
1
def menu_items
-
930
self.class.menu_items
-
end
-
-
# Returns the menu item name according to the current action
-
1
def current_menu_item
-
@current_menu_item ||= menu_items[controller_name.to_sym][:actions][action_name.to_sym] ||
-
4720
menu_items[controller_name.to_sym][:default]
-
end
-
-
# Redirects user to the menu item of the given project
-
# Returns false if user is not authorized
-
1
def redirect_to_project_menu_item(project, name)
-
item = Redmine::MenuManager.items(:project_menu).detect {|i| i.name.to_s == name.to_s}
-
if item && User.current.allowed_to?(item.url, project) && (item.condition.nil? || item.condition.call(project))
-
redirect_to({item.param => project}.merge(item.url))
-
return true
-
end
-
false
-
end
-
end
-
-
1
module MenuHelper
-
# Returns the current menu item name
-
1
def current_menu_item
-
4720
controller.current_menu_item
-
end
-
-
# Renders the application main menu
-
1
def render_main_menu(project)
-
359
render_menu((project && !project.new_record?) ? :project_menu : :application_menu, project)
-
end
-
-
1
def display_main_menu?(project)
-
359
menu_name = project && !project.new_record? ? :project_menu : :application_menu
-
359
Redmine::MenuManager.items(menu_name).children.present?
-
end
-
-
1
def render_menu(menu, project=nil)
-
1077
links = []
-
1077
menu_items_for(menu, project) do |node|
-
3724
links << render_menu_node(node, project)
-
end
-
1077
links.empty? ? nil : content_tag('ul', links.join("\n").html_safe)
-
end
-
-
1
def render_menu_node(node, project=nil)
-
3724
if node.children.present? || !node.child_menus.nil?
-
return render_menu_node_with_children(node, project)
-
else
-
3724
caption, url, selected = extract_node_details(node, project)
-
3724
return content_tag('li',
-
render_single_menu_node(node, caption, url, selected))
-
end
-
end
-
-
1
def render_menu_node_with_children(node, project=nil)
-
caption, url, selected = extract_node_details(node, project)
-
-
html = [].tap do |html|
-
html << '<li>'
-
# Parent
-
html << render_single_menu_node(node, caption, url, selected)
-
-
# Standard children
-
standard_children_list = "".html_safe.tap do |child_html|
-
node.children.each do |child|
-
child_html << render_menu_node(child, project)
-
end
-
end
-
-
html << content_tag(:ul, standard_children_list, :class => 'menu-children') unless standard_children_list.empty?
-
-
# Unattached children
-
unattached_children_list = render_unattached_children_menu(node, project)
-
html << content_tag(:ul, unattached_children_list, :class => 'menu-children unattached') unless unattached_children_list.blank?
-
-
html << '</li>'
-
end
-
return html.join("\n").html_safe
-
end
-
-
# Returns a list of unattached children menu items
-
1
def render_unattached_children_menu(node, project)
-
return nil unless node.child_menus
-
-
"".html_safe.tap do |child_html|
-
unattached_children = node.child_menus.call(project)
-
# Tree nodes support #each so we need to do object detection
-
if unattached_children.is_a? Array
-
unattached_children.each do |child|
-
child_html << content_tag(:li, render_unattached_menu_item(child, project))
-
end
-
else
-
raise MenuError, ":child_menus must be an array of MenuItems"
-
end
-
end
-
end
-
-
1
def render_single_menu_node(item, caption, url, selected)
-
3724
link_to(h(caption), url, item.html_options(:selected => selected))
-
end
-
-
1
def render_unattached_menu_item(menu_item, project)
-
raise MenuError, ":child_menus must be an array of MenuItems" unless menu_item.is_a? MenuItem
-
-
if User.current.allowed_to?(menu_item.url, project)
-
link_to(h(menu_item.caption),
-
menu_item.url,
-
menu_item.html_options)
-
end
-
end
-
-
1
def menu_items_for(menu, project=nil)
-
1077
items = []
-
1077
Redmine::MenuManager.items(menu).root.children.each do |node|
-
5318
if allowed_node?(node, User.current, project)
-
3724
if block_given?
-
3724
yield node
-
else
-
items << node # TODO: not used?
-
end
-
end
-
end
-
1077
return block_given? ? nil : items
-
end
-
-
1
def extract_node_details(node, project=nil)
-
3724
item = node
-
3724
url = case item.url
-
when Hash
-
2647
project.nil? ? item.url : {item.param => project}.merge(item.url)
-
when Symbol
-
718
send(item.url)
-
else
-
359
item.url
-
end
-
3724
caption = item.caption(project)
-
3724
return [caption, url, (current_menu_item == item.name)]
-
end
-
-
# Checks if a user is allowed to access the menu item by:
-
#
-
# * Checking the conditions of the item
-
# * Checking the url target (project only)
-
1
def allowed_node?(node, user, project)
-
5318
if node.condition && !node.condition.call(project)
-
# Condition that doesn't pass
-
1462
return false
-
end
-
-
3856
if project
-
1789
return user && user.allowed_to?(node.url, project)
-
else
-
# outside a project, all menu items allowed
-
2067
return true
-
end
-
end
-
end
-
-
1
class << self
-
1
def map(menu_name)
-
9
@items ||= {}
-
9
mapper = Mapper.new(menu_name.to_sym, @items)
-
9
if block_given?
-
5
yield mapper
-
else
-
4
mapper
-
end
-
end
-
-
1
def items(menu_name)
-
1436
@items[menu_name.to_sym] || MenuNode.new(:root, {})
-
end
-
end
-
-
1
class Mapper
-
1
def initialize(menu, items)
-
9
items[menu] ||= MenuNode.new(:root, {})
-
9
@menu = menu
-
9
@menu_items = items[menu]
-
end
-
-
# Adds an item at the end of the menu. Available options:
-
# * param: the parameter name that is used for the project id (default is :id)
-
# * if: a Proc that is called before rendering the item, the item is displayed only if it returns true
-
# * caption that can be:
-
# * a localized string Symbol
-
# * a String
-
# * a Proc that can take the project as argument
-
# * before, after: specify where the menu item should be inserted (eg. :after => :activity)
-
# * parent: menu item will be added as a child of another named menu (eg. :parent => :issues)
-
# * children: a Proc that is called before rendering the item. The Proc should return an array of MenuItems, which will be added as children to this item.
-
# eg. :children => Proc.new {|project| [Redmine::MenuManager::MenuItem.new(...)] }
-
# * last: menu item will stay at the end (eg. :last => true)
-
# * html_options: a hash of html options that are passed to link_to
-
1
def push(name, url, options={})
-
40
options = options.dup
-
-
40
if options[:parent]
-
subtree = self.find(options[:parent])
-
if subtree
-
target_root = subtree
-
else
-
target_root = @menu_items.root
-
end
-
-
else
-
40
target_root = @menu_items.root
-
end
-
-
# menu item position
-
40
if first = options.delete(:first)
-
target_root.prepend(MenuItem.new(name, url, options))
-
40
elsif before = options.delete(:before)
-
-
1
if exists?(before)
-
1
target_root.add_at(MenuItem.new(name, url, options), position_of(before))
-
else
-
target_root.add(MenuItem.new(name, url, options))
-
end
-
-
39
elsif after = options.delete(:after)
-
-
2
if exists?(after)
-
2
target_root.add_at(MenuItem.new(name, url, options), position_of(after) + 1)
-
else
-
target_root.add(MenuItem.new(name, url, options))
-
end
-
-
37
elsif options[:last] # don't delete, needs to be stored
-
5
target_root.add_last(MenuItem.new(name, url, options))
-
else
-
32
target_root.add(MenuItem.new(name, url, options))
-
end
-
end
-
-
# Removes a menu item
-
1
def delete(name)
-
if found = self.find(name)
-
@menu_items.remove!(found)
-
end
-
end
-
-
# Checks if a menu item exists
-
1
def exists?(name)
-
27
@menu_items.any? {|node| node.name == name}
-
end
-
-
1
def find(name)
-
@menu_items.find {|node| node.name == name}
-
end
-
-
1
def position_of(name)
-
3
@menu_items.each do |node|
-
24
if node.name == name
-
3
return node.position
-
end
-
end
-
end
-
end
-
-
1
class MenuNode
-
1
include Enumerable
-
1
attr_accessor :parent
-
1
attr_reader :last_items_count, :name
-
-
1
def initialize(name, content = nil)
-
45
@name = name
-
45
@children = []
-
45
@last_items_count = 0
-
end
-
-
1
def children
-
5475
if block_given?
-
584
@children.each {|child| yield child}
-
else
-
5163
@children
-
end
-
end
-
-
# Returns the number of descendants + 1
-
1
def size
-
@children.inject(1) {|sum, node| sum + node.size}
-
end
-
-
1
def each &block
-
318
yield self
-
584
children { |child| child.each(&block) }
-
end
-
-
# Adds a child at first position
-
1
def prepend(child)
-
add_at(child, 0)
-
end
-
-
# Adds a child at given position
-
1
def add_at(child, position)
-
310
raise "Child already added" if find {|node| node.name == child.name}
-
-
40
@children = @children.insert(position, child)
-
40
child.parent = self
-
40
child
-
end
-
-
# Adds a child as last child
-
1
def add_last(child)
-
5
add_at(child, -1)
-
5
@last_items_count += 1
-
5
child
-
end
-
-
# Adds a child
-
1
def add(child)
-
32
position = @children.size - @last_items_count
-
32
add_at(child, position)
-
end
-
1
alias :<< :add
-
-
# Removes a child
-
1
def remove!(child)
-
@children.delete(child)
-
@last_items_count -= +1 if child && child.last
-
child.parent = nil
-
child
-
end
-
-
# Returns the position for this node in it's parent
-
1
def position
-
3
self.parent.children.index(self)
-
end
-
-
# Returns the root for this node
-
1
def root
-
1117
root = self
-
1117
root = root.parent while root.parent
-
1117
root
-
end
-
end
-
-
1
class MenuItem < MenuNode
-
1
include Redmine::I18n
-
1
attr_reader :name, :url, :param, :condition, :parent, :child_menus, :last
-
-
1
def initialize(name, url, options)
-
40
raise ArgumentError, "Invalid option :if for menu item '#{name}'" if options[:if] && !options[:if].respond_to?(:call)
-
40
raise ArgumentError, "Invalid option :html for menu item '#{name}'" if options[:html] && !options[:html].is_a?(Hash)
-
40
raise ArgumentError, "Cannot set the :parent to be the same as this item" if options[:parent] == name.to_sym
-
40
raise ArgumentError, "Invalid option :children for menu item '#{name}'" if options[:children] && !options[:children].respond_to?(:call)
-
40
@name = name
-
40
@url = url
-
40
@condition = options[:if]
-
40
@param = options[:param] || :id
-
40
@caption = options[:caption]
-
40
@html_options = options[:html] || {}
-
# Adds a unique class to each menu item based on its name
-
40
@html_options[:class] = [@html_options[:class], @name.to_s.dasherize].compact.join(' ')
-
40
@parent = options[:parent]
-
40
@child_menus = options[:children]
-
40
@last = options[:last] || false
-
40
super @name.to_sym
-
end
-
-
1
def caption(project=nil)
-
3724
if @caption.is_a?(Proc)
-
c = @caption.call(project).to_s
-
c = @name.to_s.humanize if c.blank?
-
c
-
else
-
3724
if @caption.nil?
-
2287
l_or_humanize(name, :prefix => 'label_')
-
else
-
1437
@caption.is_a?(Symbol) ? l(@caption) : @caption
-
end
-
end
-
end
-
-
1
def html_options(options={})
-
3724
if options[:selected]
-
105
o = @html_options.dup
-
105
o[:class] += ' selected'
-
105
o
-
else
-
3619
@html_options
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module MimeType
-
-
1
MIME_TYPES = {
-
'text/plain' => 'txt,tpl,properties,patch,diff,ini,readme,install,upgrade',
-
'text/css' => 'css',
-
'text/html' => 'html,htm,xhtml',
-
'text/jsp' => 'jsp',
-
'text/x-c' => 'c,cpp,cc,h,hh',
-
'text/x-csharp' => 'cs',
-
'text/x-java' => 'java',
-
'text/x-javascript' => 'js',
-
'text/x-html-template' => 'rhtml',
-
'text/x-perl' => 'pl,pm',
-
'text/x-php' => 'php,php3,php4,php5',
-
'text/x-python' => 'py',
-
'text/x-ruby' => 'rb,rbw,ruby,rake,erb',
-
'text/x-csh' => 'csh',
-
'text/x-sh' => 'sh',
-
'text/xml' => 'xml,xsd,mxml',
-
'text/yaml' => 'yml,yaml',
-
'text/csv' => 'csv',
-
'text/x-po' => 'po',
-
'image/gif' => 'gif',
-
'image/jpeg' => 'jpg,jpeg,jpe',
-
'image/png' => 'png',
-
'image/tiff' => 'tiff,tif',
-
'image/x-ms-bmp' => 'bmp',
-
'image/x-xpixmap' => 'xpm',
-
'application/pdf' => 'pdf',
-
'application/rtf' => 'rtf',
-
'application/msword' => 'doc',
-
'application/vnd.ms-excel' => 'xls',
-
'application/vnd.ms-powerpoint' => 'ppt,pps',
-
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
-
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
-
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
-
'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => 'ppsx',
-
'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
-
'application/vnd.oasis.opendocument.text' => 'odt',
-
'application/vnd.oasis.opendocument.presentation' => 'odp',
-
'application/x-7z-compressed' => '7z',
-
'application/x-rar-compressed' => 'rar',
-
'application/x-tar' => 'tar',
-
'application/zip' => 'zip',
-
'application/x-gzip' => 'gz',
-
}.freeze
-
-
1
EXTENSIONS = MIME_TYPES.inject({}) do |map, (type, exts)|
-
113
exts.split(',').each {|ext| map[ext.strip] = type}
-
42
map
-
end
-
-
# returns mime type for name or nil if unknown
-
1
def self.of(name)
-
return nil unless name
-
m = name.to_s.match(/(^|\.)([^\.]+)$/)
-
EXTENSIONS[m[2].downcase] if m
-
end
-
-
# Returns the css class associated to
-
# the mime type of name
-
1
def self.css_class_of(name)
-
mime = of(name)
-
mime && mime.gsub('/', '-')
-
end
-
-
1
def self.main_mimetype_of(name)
-
mimetype = of(name)
-
mimetype.split('/').first if mimetype
-
end
-
-
# return true if mime-type for name is type/*
-
# otherwise false
-
1
def self.is_type?(type, name)
-
main_mimetype = main_mimetype_of(name)
-
type.to_s == main_mimetype
-
end
-
end
-
end
-
1
module Redmine
-
1
class Notifiable < Struct.new(:name, :parent)
-
-
1
def to_s
-
name
-
end
-
-
# TODO: Plugin API for adding a new notification?
-
1
def self.all
-
notifications = []
-
notifications << Notifiable.new('issue_added')
-
notifications << Notifiable.new('issue_updated')
-
notifications << Notifiable.new('issue_note_added', 'issue_updated')
-
notifications << Notifiable.new('issue_status_updated', 'issue_updated')
-
notifications << Notifiable.new('issue_priority_updated', 'issue_updated')
-
notifications << Notifiable.new('news_added')
-
notifications << Notifiable.new('news_comment_added')
-
notifications << Notifiable.new('document_added')
-
notifications << Notifiable.new('file_added')
-
notifications << Notifiable.new('message_posted')
-
notifications << Notifiable.new('wiki_content_added')
-
notifications << Notifiable.new('wiki_content_updated')
-
notifications
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine #:nodoc:
-
-
1
class PluginNotFound < StandardError; end
-
1
class PluginRequirementError < StandardError; end
-
-
# Base class for Redmine plugins.
-
# Plugins are registered using the <tt>register</tt> class method that acts as the public constructor.
-
#
-
# Redmine::Plugin.register :example do
-
# name 'Example plugin'
-
# author 'John Smith'
-
# description 'This is an example plugin for Redmine'
-
# version '0.0.1'
-
# settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
-
# end
-
#
-
# === Plugin attributes
-
#
-
# +settings+ is an optional attribute that let the plugin be configurable.
-
# It must be a hash with the following keys:
-
# * <tt>:default</tt>: default value for the plugin settings
-
# * <tt>:partial</tt>: path of the configuration partial view, relative to the plugin <tt>app/views</tt> directory
-
# Example:
-
# settings :default => {'foo'=>'bar'}, :partial => 'settings/settings'
-
# In this example, the settings partial will be found here in the plugin directory: <tt>app/views/settings/_settings.rhtml</tt>.
-
#
-
# When rendered, the plugin settings value is available as the local variable +settings+
-
1
class Plugin
-
1
cattr_accessor :directory
-
1
self.directory = File.join(Rails.root, 'plugins')
-
-
1
cattr_accessor :public_directory
-
1
self.public_directory = File.join(Rails.root, 'public', 'plugin_assets')
-
-
1
@registered_plugins = {}
-
1
class << self
-
1
attr_reader :registered_plugins
-
1
private :new
-
-
1
def def_field(*names)
-
1
class_eval do
-
1
names.each do |name|
-
7
define_method(name) do |*args|
-
3478
args.empty? ? instance_variable_get("@#{name}") : instance_variable_set("@#{name}", *args)
-
end
-
end
-
end
-
end
-
end
-
1
def_field :name, :description, :url, :author, :author_url, :version, :settings
-
1
attr_reader :id
-
-
# Plugin constructor
-
1
def self.register(id, &block)
-
2
p = new(id)
-
2
p.instance_eval(&block)
-
# Set a default name if it was not provided during registration
-
2
p.name(id.to_s.humanize) if p.name.nil?
-
-
# Adds plugin locales if any
-
# YAML translation files should be found under <plugin>/config/locales/
-
2
::I18n.load_path += Dir.glob(File.join(p.directory, 'config', 'locales', '*.yml'))
-
-
# Prepends the app/views directory of the plugin to the view path
-
2
view_path = File.join(p.directory, 'app', 'views')
-
2
if File.directory?(view_path)
-
2
ActionController::Base.prepend_view_path(view_path)
-
end
-
-
# Adds the app/{controllers,helpers,models} directories of the plugin to the autoload path
-
2
Dir.glob File.expand_path(File.join(p.directory, 'app', '{controllers,helpers,models}')) do |dir|
-
5
ActiveSupport::Dependencies.autoload_paths += [dir]
-
end
-
-
2
registered_plugins[id] = p
-
end
-
-
# Returns an array of all registered plugins
-
1
def self.all
-
2
registered_plugins.values.sort
-
end
-
-
# Finds a plugin by its id
-
# Returns a PluginNotFound exception if the plugin doesn't exist
-
1
def self.find(id)
-
3460
registered_plugins[id.to_sym] || raise(PluginNotFound)
-
end
-
-
# Clears the registered plugins hash
-
# It doesn't unload installed plugins
-
1
def self.clear
-
@registered_plugins = {}
-
end
-
-
# Checks if a plugin is installed
-
#
-
# @param [String] id name of the plugin
-
1
def self.installed?(id)
-
registered_plugins[id.to_sym].present?
-
end
-
-
1
def self.load
-
1
Dir.glob(File.join(self.directory, '*')).sort.each do |directory|
-
3
if File.directory?(directory)
-
2
lib = File.join(directory, "lib")
-
2
if File.directory?(lib)
-
2
$:.unshift lib
-
2
ActiveSupport::Dependencies.autoload_paths += [lib]
-
end
-
2
initializer = File.join(directory, "init.rb")
-
2
if File.file?(initializer)
-
2
require initializer
-
end
-
end
-
end
-
end
-
-
1
def initialize(id)
-
2
@id = id.to_sym
-
end
-
-
1
def directory
-
8
File.join(self.class.directory, id.to_s)
-
end
-
-
1
def public_directory
-
2
File.join(self.class.public_directory, id.to_s)
-
end
-
-
1
def assets_directory
-
2
File.join(directory, 'assets')
-
end
-
-
1
def <=>(plugin)
-
2
self.id.to_s <=> plugin.id.to_s
-
end
-
-
# Sets a requirement on Redmine version
-
# Raises a PluginRequirementError exception if the requirement is not met
-
#
-
# Examples
-
# # Requires Redmine 0.7.3 or higher
-
# requires_redmine :version_or_higher => '0.7.3'
-
# requires_redmine '0.7.3'
-
#
-
# # Requires a specific Redmine version
-
# requires_redmine :version => '0.7.3' # 0.7.3 only
-
# requires_redmine :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
-
1
def requires_redmine(arg)
-
1
arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
-
1
arg.assert_valid_keys(:version, :version_or_higher)
-
-
1
current = Redmine::VERSION.to_a
-
1
arg.each do |k, v|
-
1
v = [] << v unless v.is_a?(Array)
-
2
versions = v.collect {|s| s.split('.').collect(&:to_i)}
-
1
case k
-
when :version_or_higher
-
1
raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
-
1
unless (current <=> versions.first) >= 0
-
raise PluginRequirementError.new("#{id} plugin requires Redmine #{v} or higher but current is #{current.join('.')}")
-
end
-
when :version
-
unless versions.include?(current.slice(0,3))
-
raise PluginRequirementError.new("#{id} plugin requires one the following Redmine versions: #{v.join(', ')} but current is #{current.join('.')}")
-
end
-
end
-
end
-
1
true
-
end
-
-
# Sets a requirement on a Redmine plugin version
-
# Raises a PluginRequirementError exception if the requirement is not met
-
#
-
# Examples
-
# # Requires a plugin named :foo version 0.7.3 or higher
-
# requires_redmine_plugin :foo, :version_or_higher => '0.7.3'
-
# requires_redmine_plugin :foo, '0.7.3'
-
#
-
# # Requires a specific version of a Redmine plugin
-
# requires_redmine_plugin :foo, :version => '0.7.3' # 0.7.3 only
-
# requires_redmine_plugin :foo, :version => ['0.7.3', '0.8.0'] # 0.7.3 or 0.8.0
-
1
def requires_redmine_plugin(plugin_name, arg)
-
arg = { :version_or_higher => arg } unless arg.is_a?(Hash)
-
arg.assert_valid_keys(:version, :version_or_higher)
-
-
plugin = Plugin.find(plugin_name)
-
current = plugin.version.split('.').collect(&:to_i)
-
-
arg.each do |k, v|
-
v = [] << v unless v.is_a?(Array)
-
versions = v.collect {|s| s.split('.').collect(&:to_i)}
-
case k
-
when :version_or_higher
-
raise ArgumentError.new("wrong number of versions (#{versions.size} for 1)") unless versions.size == 1
-
unless (current <=> versions.first) >= 0
-
raise PluginRequirementError.new("#{id} plugin requires the #{plugin_name} plugin #{v} or higher but current is #{current.join('.')}")
-
end
-
when :version
-
unless versions.include?(current.slice(0,3))
-
raise PluginRequirementError.new("#{id} plugin requires one the following versions of #{plugin_name}: #{v.join(', ')} but current is #{current.join('.')}")
-
end
-
end
-
end
-
true
-
end
-
-
# Adds an item to the given +menu+.
-
# The +id+ parameter (equals to the project id) is automatically added to the url.
-
# menu :project_menu, :plugin_example, { :controller => 'example', :action => 'say_hello' }, :caption => 'Sample'
-
#
-
# +name+ parameter can be: :top_menu, :account_menu, :application_menu or :project_menu
-
#
-
1
def menu(menu, item, url, options={})
-
4
Redmine::MenuManager.map(menu).push(item, url, options)
-
end
-
1
alias :add_menu_item :menu
-
-
# Removes +item+ from the given +menu+.
-
1
def delete_menu_item(menu, item)
-
Redmine::MenuManager.map(menu).delete(item)
-
end
-
-
# Defines a permission called +name+ for the given +actions+.
-
#
-
# The +actions+ argument is a hash with controllers as keys and actions as values (a single value or an array):
-
# permission :destroy_contacts, { :contacts => :destroy }
-
# permission :view_contacts, { :contacts => [:index, :show] }
-
#
-
# The +options+ argument can be used to make the permission public (implicitly given to any user)
-
# or to restrict users the permission can be given to.
-
#
-
# Examples
-
# # A permission that is implicitly given to any user
-
# # This permission won't appear on the Roles & Permissions setup screen
-
# permission :say_hello, { :example => :say_hello }, :public => true
-
#
-
# # A permission that can be given to any user
-
# permission :say_hello, { :example => :say_hello }
-
#
-
# # A permission that can be given to registered users only
-
# permission :say_hello, { :example => :say_hello }, :require => :loggedin
-
#
-
# # A permission that can be given to project members only
-
# permission :say_hello, { :example => :say_hello }, :require => :member
-
1
def permission(name, actions, options = {})
-
21
if @project_module
-
63
Redmine::AccessControl.map {|map| map.project_module(@project_module) {|map|map.permission(name, actions, options)}}
-
else
-
Redmine::AccessControl.map {|map| map.permission(name, actions, options)}
-
end
-
end
-
-
# Defines a project module, that can be enabled/disabled for each project.
-
# Permissions defined inside +block+ will be bind to the module.
-
#
-
# project_module :things do
-
# permission :view_contacts, { :contacts => [:list, :show] }, :public => true
-
# permission :destroy_contacts, { :contacts => :destroy }
-
# end
-
1
def project_module(name, &block)
-
2
@project_module = name
-
2
self.instance_eval(&block)
-
2
@project_module = nil
-
end
-
-
# Registers an activity provider.
-
#
-
# Options:
-
# * <tt>:class_name</tt> - one or more model(s) that provide these events (inferred from event_type by default)
-
# * <tt>:default</tt> - setting this option to false will make the events not displayed by default
-
#
-
# A model can provide several activity event types.
-
#
-
# Examples:
-
# register :news
-
# register :scrums, :class_name => 'Meeting'
-
# register :issues, :class_name => ['Issue', 'Journal']
-
#
-
# Retrieving events:
-
# Associated model(s) must implement the find_events class method.
-
# ActiveRecord models can use acts_as_activity_provider as a way to implement this class method.
-
#
-
# The following call should return all the scrum events visible by current user that occured in the 5 last days:
-
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today)
-
# Meeting.find_events('scrums', User.current, 5.days.ago, Date.today, :project => foo) # events for project foo only
-
#
-
# Note that :view_scrums permission is required to view these events in the activity view.
-
1
def activity_provider(*args)
-
Redmine::Activity.register(*args)
-
end
-
-
# Registers a wiki formatter.
-
#
-
# Parameters:
-
# * +name+ - human-readable name
-
# * +formatter+ - formatter class, which should have an instance method +to_html+
-
# * +helper+ - helper module, which will be included by wiki pages
-
1
def wiki_format_provider(name, formatter, helper)
-
Redmine::WikiFormatting.register(name, formatter, helper)
-
end
-
-
# Returns +true+ if the plugin can be configured.
-
1
def configurable?
-
settings && settings.is_a?(Hash) && !settings[:partial].blank?
-
end
-
-
1
def mirror_assets
-
2
source = assets_directory
-
2
destination = public_directory
-
2
return unless File.directory?(source)
-
-
1
source_files = Dir[source + "/**/*"]
-
427
source_dirs = source_files.select { |d| File.directory?(d) }
-
1
source_files -= source_dirs
-
-
1
unless source_files.empty?
-
1
base_target_dir = File.join(destination, File.dirname(source_files.first).gsub(source, ''))
-
1
FileUtils.mkdir_p(base_target_dir)
-
end
-
-
1
source_dirs.each do |dir|
-
# strip down these paths so we have simple, relative paths we can
-
# add to the destination
-
22
target_dir = File.join(destination, dir.gsub(source, ''))
-
22
begin
-
22
FileUtils.mkdir_p(target_dir)
-
rescue Exception => e
-
raise "Could not create directory #{target_dir}: \n" + e
-
end
-
end
-
-
1
source_files.each do |file|
-
404
begin
-
404
target = File.join(destination, file.gsub(source, ''))
-
404
unless File.exist?(target) && FileUtils.identical?(file, target)
-
FileUtils.cp(file, target)
-
end
-
rescue Exception => e
-
raise "Could not copy #{file} to #{target}: \n" + e
-
end
-
end
-
end
-
-
# Mirrors assets from one or all plugins to public/plugin_assets
-
1
def self.mirror_assets(name=nil)
-
1
if name.present?
-
find(name).mirror_assets
-
else
-
1
all.each do |plugin|
-
2
plugin.mirror_assets
-
end
-
end
-
end
-
-
# The directory containing this plugin's migrations (<tt>plugin/db/migrate</tt>)
-
1
def migration_directory
-
File.join(Rails.root, 'plugins', id.to_s, 'db', 'migrate')
-
end
-
-
# Returns the version number of the latest migration for this plugin. Returns
-
# nil if this plugin has no migrations.
-
1
def latest_migration
-
migrations.last
-
end
-
-
# Returns the version numbers of all migrations for this plugin.
-
1
def migrations
-
migrations = Dir[migration_directory+"/*.rb"]
-
migrations.map { |p| File.basename(p).match(/0*(\d+)\_/)[1].to_i }.sort
-
end
-
-
# Migrate this plugin to the given version
-
1
def migrate(version = nil)
-
puts "Migrating #{id} (#{name})..."
-
Redmine::Plugin::Migrator.migrate_plugin(self, version)
-
end
-
-
# Migrates all plugins or a single plugin to a given version
-
# Exemples:
-
# Plugin.migrate
-
# Plugin.migrate('sample_plugin')
-
# Plugin.migrate('sample_plugin', 1)
-
#
-
1
def self.migrate(name=nil, version=nil)
-
if name.present?
-
find(name).migrate(version)
-
else
-
all.each do |plugin|
-
plugin.migrate
-
end
-
end
-
end
-
-
1
class Migrator < ActiveRecord::Migrator
-
# We need to be able to set the 'current' plugin being migrated.
-
1
cattr_accessor :current_plugin
-
-
1
class << self
-
# Runs the migrations from a plugin, up (or down) to the version given
-
1
def migrate_plugin(plugin, version)
-
self.current_plugin = plugin
-
return if current_version(plugin) == version
-
migrate(plugin.migration_directory, version)
-
end
-
-
1
def current_version(plugin=current_plugin)
-
# Delete migrations that don't match .. to_i will work because the number comes first
-
::ActiveRecord::Base.connection.select_values(
-
"SELECT version FROM #{schema_migrations_table_name}"
-
).delete_if{ |v| v.match(/-#{plugin.id}/) == nil }.map(&:to_i).max || 0
-
end
-
end
-
-
1
def migrated
-
sm_table = self.class.schema_migrations_table_name
-
::ActiveRecord::Base.connection.select_values(
-
"SELECT version FROM #{sm_table}"
-
).delete_if{ |v| v.match(/-#{current_plugin.id}/) == nil }.map(&:to_i).sort
-
end
-
-
1
def record_version_state_after_migrating(version)
-
super(version.to_s + "-" + current_plugin.id.to_s)
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module SafeAttributes
-
1
def self.included(base)
-
13
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
# Declares safe attributes
-
# An optional Proc can be given for conditional inclusion
-
#
-
# Example:
-
# safe_attributes 'title', 'pages'
-
# safe_attributes 'isbn', :if => {|book, user| book.author == user}
-
1
def safe_attributes(*args)
-
268
@safe_attributes ||= []
-
268
if args.empty?
-
244
@safe_attributes
-
else
-
24
options = args.last.is_a?(Hash) ? args.pop : {}
-
24
@safe_attributes << [args, options]
-
end
-
end
-
end
-
-
# Returns an array that can be safely set by user or current user
-
#
-
# Example:
-
# book.safe_attributes # => ['title', 'pages']
-
# book.safe_attributes(book.author) # => ['title', 'pages', 'isbn']
-
1
def safe_attribute_names(user=nil)
-
306
return @safe_attribute_names if @safe_attribute_names && user.nil?
-
244
names = []
-
244
self.class.safe_attributes.collect do |attrs, options|
-
1938
if options[:if].nil? || options[:if].call(self, user || User.current)
-
1416
names += attrs.collect(&:to_s)
-
end
-
end
-
244
names.uniq!
-
244
@safe_attribute_names = names if user.nil?
-
244
names
-
end
-
-
# Returns true if attr can be set by user or the current user
-
1
def safe_attribute?(attr, user=nil)
-
66
safe_attribute_names(user).include?(attr.to_s)
-
end
-
-
# Returns a hash with unsafe attributes removed
-
# from the given attrs hash
-
#
-
# Example:
-
# book.delete_unsafe_attributes({'title' => 'My book', 'foo' => 'bar'})
-
# # => {'title' => 'My book'}
-
1
def delete_unsafe_attributes(attrs, user=User.current)
-
4
safe = safe_attribute_names(user)
-
50
attrs.dup.delete_if {|k,v| !safe.include?(k)}
-
end
-
-
# Sets attributes from attrs that are safe
-
# attrs is a Hash with string keys
-
1
def safe_attributes=(attrs, user=User.current)
-
4
return unless attrs.is_a?(Hash)
-
2
self.attributes = delete_unsafe_attributes(attrs, user)
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'cgi'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class CommandFailed < StandardError #:nodoc:
-
end
-
-
1
class AbstractAdapter #:nodoc:
-
-
# raised if scm command exited with error, e.g. unknown revision.
-
1
class ScmCommandAborted < CommandFailed; end
-
-
1
class << self
-
1
def client_command
-
""
-
end
-
-
1
def shell_quote_command
-
if Redmine::Platform.mswin? && RUBY_PLATFORM == 'java'
-
client_command
-
else
-
shell_quote(client_command)
-
end
-
end
-
-
# Returns the version of the scm client
-
# Eg: [1, 5, 0] or [] if unknown
-
1
def client_version
-
[]
-
end
-
-
# Returns the version string of the scm client
-
# Eg: '1.5.0' or 'Unknown version' if unknown
-
1
def client_version_string
-
v = client_version || 'Unknown version'
-
v.is_a?(Array) ? v.join('.') : v.to_s
-
end
-
-
# Returns true if the current client version is above
-
# or equals the given one
-
# If option is :unknown is set to true, it will return
-
# true if the client version is unknown
-
1
def client_version_above?(v, options={})
-
((client_version <=> v) >= 0) || (client_version.empty? && options[:unknown])
-
end
-
-
1
def client_available
-
true
-
end
-
-
1
def shell_quote(str)
-
if Redmine::Platform.mswin?
-
'"' + str.gsub(/"/, '\\"') + '"'
-
else
-
"'" + str.gsub(/'/, "'\"'\"'") + "'"
-
end
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@url = url
-
@login = login if login && !login.empty?
-
@password = (password || "") if @login
-
@root_url = root_url.blank? ? retrieve_root_url : root_url
-
end
-
-
1
def adapter_name
-
'Abstract'
-
end
-
-
1
def supports_cat?
-
true
-
end
-
-
1
def supports_annotate?
-
respond_to?('annotate')
-
end
-
-
1
def root_url
-
@root_url
-
end
-
-
1
def url
-
@url
-
end
-
-
1
def path_encoding
-
nil
-
end
-
-
# get info about the svn repository
-
1
def info
-
return nil
-
end
-
-
# Returns the entry identified by path and revision identifier
-
# or nil if entry doesn't exist in the repository
-
1
def entry(path=nil, identifier=nil)
-
parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
-
search_path = parts[0..-2].join('/')
-
search_name = parts[-1]
-
if search_path.blank? && search_name.blank?
-
# Root entry
-
Entry.new(:path => '', :kind => 'dir')
-
else
-
# Search for the entry in the parent directory
-
es = entries(search_path, identifier)
-
es ? es.detect {|e| e.name == search_name} : nil
-
end
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
return nil
-
end
-
-
1
def branches
-
return nil
-
end
-
-
1
def tags
-
return nil
-
end
-
-
1
def default_branch
-
return nil
-
end
-
-
1
def properties(path, identifier=nil)
-
return nil
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
return nil
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
return nil
-
end
-
-
1
def cat(path, identifier=nil)
-
return nil
-
end
-
-
1
def with_leading_slash(path)
-
path ||= ''
-
(path[0,1]!="/") ? "/#{path}" : path
-
end
-
-
1
def with_trailling_slash(path)
-
path ||= ''
-
(path[-1,1] == "/") ? path : "#{path}/"
-
end
-
-
1
def without_leading_slash(path)
-
path ||= ''
-
path.gsub(%r{^/+}, '')
-
end
-
-
1
def without_trailling_slash(path)
-
path ||= ''
-
(path[-1,1] == "/") ? path[0..-2] : path
-
end
-
-
1
def shell_quote(str)
-
self.class.shell_quote(str)
-
end
-
-
1
private
-
1
def retrieve_root_url
-
info = self.info
-
info ? info.root_url : nil
-
end
-
-
1
def target(path, sq=true)
-
path ||= ''
-
base = path.match(/^\//) ? root_url : url
-
str = "#{base}/#{path}".gsub(/[?<>\*]/, '')
-
if sq
-
str = shell_quote(str)
-
end
-
str
-
end
-
-
1
def logger
-
self.class.logger
-
end
-
-
1
def shellout(cmd, options = {}, &block)
-
self.class.shellout(cmd, options, &block)
-
end
-
-
1
def self.logger
-
Rails.logger
-
end
-
-
1
def self.shellout(cmd, options = {}, &block)
-
if logger && logger.debug?
-
logger.debug "Shelling out: #{strip_credential(cmd)}"
-
end
-
if Rails.env == 'development'
-
# Capture stderr when running in dev environment
-
cmd = "#{cmd} 2>>#{shell_quote(Rails.root.join('log/scm.stderr.log').to_s)}"
-
end
-
begin
-
mode = "r+"
-
IO.popen(cmd, mode) do |io|
-
io.set_encoding("ASCII-8BIT") if io.respond_to?(:set_encoding)
-
io.close_write unless options[:write_stdin]
-
block.call(io) if block_given?
-
end
-
## If scm command does not exist,
-
## Linux JRuby 1.6.2 (ruby-1.8.7-p330) raises java.io.IOException
-
## in production environment.
-
# rescue Errno::ENOENT => e
-
rescue Exception => e
-
msg = strip_credential(e.message)
-
# The command failed, log it and re-raise
-
logmsg = "SCM command failed, "
-
logmsg += "make sure that your SCM command (e.g. svn) is "
-
logmsg += "in PATH (#{ENV['PATH']})\n"
-
logmsg += "You can configure your scm commands in config/configuration.yml.\n"
-
logmsg += "#{strip_credential(cmd)}\n"
-
logmsg += "with: #{msg}"
-
logger.error(logmsg)
-
raise CommandFailed.new(msg)
-
end
-
end
-
-
# Hides username/password in a given command
-
1
def self.strip_credential(cmd)
-
q = (Redmine::Platform.mswin? ? '"' : "'")
-
cmd.to_s.gsub(/(\-\-(password|username))\s+(#{q}[^#{q}]+#{q}|[^#{q}]\S+)/, '\\1 xxxx')
-
end
-
-
1
def strip_credential(cmd)
-
self.class.strip_credential(cmd)
-
end
-
-
1
def scm_iconv(to, from, str)
-
return nil if str.nil?
-
return str if to == from
-
begin
-
Iconv.conv(to, from, str)
-
rescue Iconv::Failure => err
-
logger.error("failed to convert from #{from} to #{to}. #{err}")
-
nil
-
end
-
end
-
-
1
def parse_xml(xml)
-
if RUBY_PLATFORM == 'java'
-
xml = xml.sub(%r{<\?xml[^>]*\?>}, '')
-
end
-
ActiveSupport::XmlMini.parse(xml)
-
end
-
end
-
-
1
class Entries < Array
-
1
def sort_by_name
-
sort {|x,y|
-
if x.kind == y.kind
-
x.name.to_s <=> y.name.to_s
-
else
-
x.kind <=> y.kind
-
end
-
}
-
end
-
-
1
def revisions
-
revisions ||= Revisions.new(collect{|entry| entry.lastrev}.compact)
-
end
-
end
-
-
1
class Info
-
1
attr_accessor :root_url, :lastrev
-
1
def initialize(attributes={})
-
self.root_url = attributes[:root_url] if attributes[:root_url]
-
self.lastrev = attributes[:lastrev]
-
end
-
end
-
-
1
class Entry
-
1
attr_accessor :name, :path, :kind, :size, :lastrev
-
1
def initialize(attributes={})
-
self.name = attributes[:name] if attributes[:name]
-
self.path = attributes[:path] if attributes[:path]
-
self.kind = attributes[:kind] if attributes[:kind]
-
self.size = attributes[:size].to_i if attributes[:size]
-
self.lastrev = attributes[:lastrev]
-
end
-
-
1
def is_file?
-
'file' == self.kind
-
end
-
-
1
def is_dir?
-
'dir' == self.kind
-
end
-
-
1
def is_text?
-
Redmine::MimeType.is_type?('text', name)
-
end
-
end
-
-
1
class Revisions < Array
-
1
def latest
-
sort {|x,y|
-
unless x.time.nil? or y.time.nil?
-
x.time <=> y.time
-
else
-
0
-
end
-
}.last
-
end
-
end
-
-
1
class Revision
-
1
attr_accessor :scmid, :name, :author, :time, :message,
-
:paths, :revision, :branch, :identifier,
-
:parents
-
-
1
def initialize(attributes={})
-
self.identifier = attributes[:identifier]
-
self.scmid = attributes[:scmid]
-
self.name = attributes[:name] || self.identifier
-
self.author = attributes[:author]
-
self.time = attributes[:time]
-
self.message = attributes[:message] || ""
-
self.paths = attributes[:paths]
-
self.revision = attributes[:revision]
-
self.branch = attributes[:branch]
-
self.parents = attributes[:parents]
-
end
-
-
# Returns the readable identifier.
-
1
def format_identifier
-
self.identifier.to_s
-
end
-
end
-
-
1
class Annotate
-
1
attr_reader :lines, :revisions
-
-
1
def initialize
-
@lines = []
-
@revisions = []
-
end
-
-
1
def add_line(line, revision)
-
@lines << line
-
@revisions << revision
-
end
-
-
1
def content
-
content = lines.join("\n")
-
end
-
-
1
def empty?
-
lines.empty?
-
end
-
end
-
-
1
class Branch < String
-
1
attr_accessor :revision, :scmid
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class BazaarAdapter < AbstractAdapter
-
-
# Bazaar executable name
-
1
BZR_BIN = Redmine::Configuration['scm_bazaar_command'] || "bzr"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= BZR_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (scm_command_version || [])
-
end
-
-
1
def client_available
-
!client_version.empty?
-
end
-
-
1
def scm_command_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
# Get info about the repository
-
1
def info
-
cmd_args = %w|revno|
-
cmd_args << bzr_target('')
-
info = nil
-
scm_cmd(*cmd_args) do |io|
-
if io.read =~ %r{^(\d+)\r?$}
-
info = Info.new({:root_url => url,
-
:lastrev => Revision.new({
-
:identifier => $1
-
})
-
})
-
end
-
end
-
info
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
path ||= ''
-
entries = Entries.new
-
identifier = -1 unless identifier && identifier.to_i > 0
-
cmd_args = %w|ls -v --show-ids|
-
cmd_args << "-r#{identifier.to_i}"
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
prefix = "#{url}/#{path}".gsub('\\', '/')
-
logger.debug "PREFIX: #{prefix}"
-
re = %r{^V\s+(#{Regexp.escape(prefix)})?(\/?)([^\/]+)(\/?)\s+(\S+)\r?$}
-
io.each_line do |line|
-
next unless line =~ re
-
entries << Entry.new({:name => $3.strip,
-
:path => ((path.empty? ? "" : "#{path}/") + $3.strip),
-
:kind => ($4.blank? ? 'file' : 'dir'),
-
:size => nil,
-
:lastrev => Revision.new(:revision => $5.strip)
-
})
-
end
-
end
-
if logger && logger.debug?
-
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}")
-
end
-
entries.sort_by_name
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
path ||= ''
-
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : 'last:1'
-
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : 1
-
revisions = Revisions.new
-
cmd_args = %w|log -v --show-ids|
-
cmd_args << "-r#{identifier_to}..#{identifier_from}"
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
revision = nil
-
parsing = nil
-
io.each_line do |line|
-
if line =~ /^----/
-
revisions << revision if revision
-
revision = Revision.new(:paths => [], :message => '')
-
parsing = nil
-
else
-
next unless revision
-
if line =~ /^revno: (\d+)($|\s\[merge\]$)/
-
revision.identifier = $1.to_i
-
elsif line =~ /^committer: (.+)$/
-
revision.author = $1.strip
-
elsif line =~ /^revision-id:(.+)$/
-
revision.scmid = $1.strip
-
elsif line =~ /^timestamp: (.+)$/
-
revision.time = Time.parse($1).localtime
-
elsif line =~ /^ -----/
-
# partial revisions
-
parsing = nil unless parsing == 'message'
-
elsif line =~ /^(message|added|modified|removed|renamed):/
-
parsing = $1
-
elsif line =~ /^ (.*)$/
-
if parsing == 'message'
-
revision.message << "#{$1}\n"
-
else
-
if $1 =~ /^(.*)\s+(\S+)$/
-
path = $1.strip
-
revid = $2
-
case parsing
-
when 'added'
-
revision.paths << {:action => 'A', :path => "/#{path}", :revision => revid}
-
when 'modified'
-
revision.paths << {:action => 'M', :path => "/#{path}", :revision => revid}
-
when 'removed'
-
revision.paths << {:action => 'D', :path => "/#{path}", :revision => revid}
-
when 'renamed'
-
new_path = path.split('=>').last
-
revision.paths << {:action => 'M', :path => "/#{new_path.strip}", :revision => revid} if new_path
-
end
-
end
-
end
-
else
-
parsing = nil
-
end
-
end
-
end
-
revisions << revision if revision
-
end
-
revisions
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path ||= ''
-
if identifier_to
-
identifier_to = identifier_to.to_i
-
else
-
identifier_to = identifier_from.to_i - 1
-
end
-
if identifier_from
-
identifier_from = identifier_from.to_i
-
end
-
diff = []
-
cmd_args = %w|diff|
-
cmd_args << "-r#{identifier_to}..#{identifier_from}"
-
cmd_args << bzr_target(path)
-
scm_cmd_no_raise(*cmd_args) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
end
-
-
1
def cat(path, identifier=nil)
-
cat = nil
-
cmd_args = %w|cat|
-
cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
io.binmode
-
cat = io.read
-
end
-
cat
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def annotate(path, identifier=nil)
-
blame = Annotate.new
-
cmd_args = %w|annotate -q --all|
-
cmd_args << "-r#{identifier.to_i}" if identifier && identifier.to_i > 0
-
cmd_args << bzr_target(path)
-
scm_cmd(*cmd_args) do |io|
-
author = nil
-
identifier = nil
-
io.each_line do |line|
-
next unless line =~ %r{^(\d+) ([^|]+)\| (.*)$}
-
rev = $1
-
blame.add_line($3.rstrip,
-
Revision.new(
-
:identifier => rev,
-
:revision => rev,
-
:author => $2.strip
-
))
-
end
-
end
-
blame
-
rescue ScmCommandAborted
-
return nil
-
end
-
-
1
def self.branch_conf_path(path)
-
bcp = nil
-
m = path.match(%r{^(.*[/\\])\.bzr.*$})
-
if m
-
bcp = m[1]
-
else
-
bcp = path
-
end
-
bcp.gsub!(%r{[\/\\]$}, "")
-
if bcp
-
bcp = File.join(bcp, ".bzr", "branch", "branch.conf")
-
end
-
bcp
-
end
-
-
1
def append_revisions_only
-
return @aro if ! @aro.nil?
-
@aro = false
-
bcp = self.class.branch_conf_path(url)
-
if bcp && File.exist?(bcp)
-
begin
-
f = File::open(bcp, "r")
-
cnt = 0
-
f.each_line do |line|
-
l = line.chomp.to_s
-
if l =~ /^\s*append_revisions_only\s*=\s*(\w+)\s*$/
-
str_aro = $1
-
if str_aro.upcase == "TRUE"
-
@aro = true
-
cnt += 1
-
elsif str_aro.upcase == "FALSE"
-
@aro = false
-
cnt += 1
-
end
-
if cnt > 1
-
@aro = false
-
break
-
end
-
end
-
end
-
ensure
-
f.close
-
end
-
end
-
@aro
-
end
-
-
1
def scm_cmd(*args, &block)
-
full_args = []
-
full_args += args
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise ScmCommandAborted, "bzr exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :scm_cmd
-
-
1
def scm_cmd_no_raise(*args, &block)
-
full_args = []
-
full_args += args
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
ret
-
end
-
1
private :scm_cmd_no_raise
-
-
1
def bzr_target(path)
-
target(path, false)
-
end
-
1
private :bzr_target
-
end
-
end
-
end
-
end
-
# redMine - project management software
-
# Copyright (C) 2006-2007 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class CvsAdapter < AbstractAdapter
-
-
# CVS executable name
-
1
CVS_BIN = Redmine::Configuration['scm_cvs_command'] || "cvs"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= CVS_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (scm_command_version || [])
-
end
-
-
1
def client_available
-
client_version_above?([1, 12])
-
end
-
-
1
def scm_command_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)}m)
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
# Guidelines for the input:
-
# url -> the project-path, relative to the cvsroot (eg. module name)
-
# root_url -> the good old, sometimes damned, CVSROOT
-
# login -> unnecessary
-
# password -> unnecessary too
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
@url = url
-
# TODO: better Exception here (IllegalArgumentException)
-
raise CommandFailed if root_url.blank?
-
@root_url = root_url
-
-
# These are unused.
-
@login = login if login && !login.empty?
-
@password = (password || "") if @login
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def info
-
logger.debug "<cvs> info"
-
Info.new({:root_url => @root_url, :lastrev => nil})
-
end
-
-
1
def get_previous_revision(revision)
-
CvsRevisionHelper.new(revision).prevRev
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
# this method is used by the repository-browser (aka LIST)
-
1
def entries(path=nil, identifier=nil, options={})
-
logger.debug "<cvs> entries '#{path}' with identifier '#{identifier}'"
-
path_locale = scm_iconv(@path_encoding, 'UTF-8', path)
-
path_locale.force_encoding("ASCII-8BIT") if path_locale.respond_to?(:force_encoding)
-
entries = Entries.new
-
cmd_args = %w|-q rls -e|
-
cmd_args << "-D" << time_to_cvstime_rlog(identifier) if identifier
-
cmd_args << path_with_proj(path)
-
scm_cmd(*cmd_args) do |io|
-
io.each_line() do |line|
-
fields = line.chop.split('/',-1)
-
logger.debug(">>InspectLine #{fields.inspect}")
-
if fields[0]!="D"
-
time = nil
-
# Thu Dec 13 16:27:22 2007
-
time_l = fields[-3].split(' ')
-
if time_l.size == 5 && time_l[4].length == 4
-
begin
-
time = Time.parse(
-
"#{time_l[1]} #{time_l[2]} #{time_l[3]} GMT #{time_l[4]}")
-
rescue
-
end
-
end
-
entries << Entry.new(
-
{
-
:name => scm_iconv('UTF-8', @path_encoding, fields[-5]),
-
#:path => fields[-4].include?(path)?fields[-4]:(path + "/"+ fields[-4]),
-
:path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[-5]}"),
-
:kind => 'file',
-
:size => nil,
-
:lastrev => Revision.new(
-
{
-
:revision => fields[-4],
-
:name => scm_iconv('UTF-8', @path_encoding, fields[-4]),
-
:time => time,
-
:author => ''
-
})
-
})
-
else
-
entries << Entry.new(
-
{
-
:name => scm_iconv('UTF-8', @path_encoding, fields[1]),
-
:path => scm_iconv('UTF-8', @path_encoding, "#{path_locale}/#{fields[1]}"),
-
:kind => 'dir',
-
:size => nil,
-
:lastrev => nil
-
})
-
end
-
end
-
end
-
entries.sort_by_name
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
STARTLOG="----------------------------"
-
1
ENDLOG ="============================================================================="
-
-
# Returns all revisions found between identifier_from and identifier_to
-
# in the repository. both identifier have to be dates or nil.
-
# these method returns nothing but yield every result in block
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={}, &block)
-
path_with_project_utf8 = path_with_proj(path)
-
path_with_project_locale = scm_iconv(@path_encoding, 'UTF-8', path_with_project_utf8)
-
logger.debug "<cvs> revisions path:" +
-
"'#{path}',identifier_from #{identifier_from}, identifier_to #{identifier_to}"
-
cmd_args = %w|-q rlog|
-
cmd_args << "-d" << ">#{time_to_cvstime_rlog(identifier_from)}" if identifier_from
-
cmd_args << path_with_project_utf8
-
scm_cmd(*cmd_args) do |io|
-
state = "entry_start"
-
commit_log = String.new
-
revision = nil
-
date = nil
-
author = nil
-
entry_path = nil
-
entry_name = nil
-
file_state = nil
-
branch_map = nil
-
io.each_line() do |line|
-
if state != "revision" && /^#{ENDLOG}/ =~ line
-
commit_log = String.new
-
revision = nil
-
state = "entry_start"
-
end
-
if state == "entry_start"
-
branch_map = Hash.new
-
if /^RCS file: #{Regexp.escape(root_url_path)}\/#{Regexp.escape(path_with_project_locale)}(.+),v$/ =~ line
-
entry_path = normalize_cvs_path($1)
-
entry_name = normalize_path(File.basename($1))
-
logger.debug("Path #{entry_path} <=> Name #{entry_name}")
-
elsif /^head: (.+)$/ =~ line
-
entry_headRev = $1 #unless entry.nil?
-
elsif /^symbolic names:/ =~ line
-
state = "symbolic" #unless entry.nil?
-
elsif /^#{STARTLOG}/ =~ line
-
commit_log = String.new
-
state = "revision"
-
end
-
next
-
elsif state == "symbolic"
-
if /^(.*):\s(.*)/ =~ (line.strip)
-
branch_map[$1] = $2
-
else
-
state = "tags"
-
next
-
end
-
elsif state == "tags"
-
if /^#{STARTLOG}/ =~ line
-
commit_log = ""
-
state = "revision"
-
elsif /^#{ENDLOG}/ =~ line
-
state = "head"
-
end
-
next
-
elsif state == "revision"
-
if /^#{ENDLOG}/ =~ line || /^#{STARTLOG}/ =~ line
-
if revision
-
revHelper = CvsRevisionHelper.new(revision)
-
revBranch = "HEAD"
-
branch_map.each() do |branch_name, branch_point|
-
if revHelper.is_in_branch_with_symbol(branch_point)
-
revBranch = branch_name
-
end
-
end
-
logger.debug("********** YIELD Revision #{revision}::#{revBranch}")
-
yield Revision.new({
-
:time => date,
-
:author => author,
-
:message => commit_log.chomp,
-
:paths => [{
-
:revision => revision.dup,
-
:branch => revBranch.dup,
-
:path => scm_iconv('UTF-8', @path_encoding, entry_path),
-
:name => scm_iconv('UTF-8', @path_encoding, entry_name),
-
:kind => 'file',
-
:action => file_state
-
}]
-
})
-
end
-
commit_log = String.new
-
revision = nil
-
if /^#{ENDLOG}/ =~ line
-
state = "entry_start"
-
end
-
next
-
end
-
-
if /^branches: (.+)$/ =~ line
-
# TODO: version.branch = $1
-
elsif /^revision (\d+(?:\.\d+)+).*$/ =~ line
-
revision = $1
-
elsif /^date:\s+(\d+.\d+.\d+\s+\d+:\d+:\d+)/ =~ line
-
date = Time.parse($1)
-
line_utf8 = scm_iconv('UTF-8', options[:log_encoding], line)
-
author_utf8 = /author: ([^;]+)/.match(line_utf8)[1]
-
author = scm_iconv(options[:log_encoding], 'UTF-8', author_utf8)
-
file_state = /state: ([^;]+)/.match(line)[1]
-
# TODO:
-
# linechanges only available in CVS....
-
# maybe a feature our SVN implementation.
-
# I'm sure, they are useful for stats or something else
-
# linechanges =/lines: \+(\d+) -(\d+)/.match(line)
-
# unless linechanges.nil?
-
# version.line_plus = linechanges[1]
-
# version.line_minus = linechanges[2]
-
# else
-
# version.line_plus = 0
-
# version.line_minus = 0
-
# end
-
else
-
commit_log << line unless line =~ /^\*\*\* empty log message \*\*\*/
-
end
-
end
-
end
-
end
-
rescue ScmCommandAborted
-
Revisions.new
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
logger.debug "<cvs> diff path:'#{path}'" +
-
",identifier_from #{identifier_from}, identifier_to #{identifier_to}"
-
cmd_args = %w|rdiff -u|
-
cmd_args << "-r#{identifier_to}"
-
cmd_args << "-r#{identifier_from}"
-
cmd_args << path_with_proj(path)
-
diff = []
-
scm_cmd(*cmd_args) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def cat(path, identifier=nil)
-
identifier = (identifier) ? identifier : "HEAD"
-
logger.debug "<cvs> cat path:'#{path}',identifier #{identifier}"
-
cmd_args = %w|-q co|
-
cmd_args << "-D" << time_to_cvstime(identifier) if identifier
-
cmd_args << "-p" << path_with_proj(path)
-
cat = nil
-
scm_cmd(*cmd_args) do |io|
-
io.binmode
-
cat = io.read
-
end
-
cat
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def annotate(path, identifier=nil)
-
identifier = (identifier) ? identifier : "HEAD"
-
logger.debug "<cvs> annotate path:'#{path}',identifier #{identifier}"
-
cmd_args = %w|rannotate|
-
cmd_args << "-D" << time_to_cvstime(identifier) if identifier
-
cmd_args << path_with_proj(path)
-
blame = Annotate.new
-
scm_cmd(*cmd_args) do |io|
-
io.each_line do |line|
-
next unless line =~ %r{^([\d\.]+)\s+\(([^\)]+)\s+[^\)]+\):\s(.*)$}
-
blame.add_line(
-
$3.rstrip,
-
Revision.new(
-
:revision => $1,
-
:identifier => nil,
-
:author => $2.strip
-
))
-
end
-
end
-
blame
-
rescue ScmCommandAborted
-
Annotate.new
-
end
-
-
1
private
-
-
# Returns the root url without the connexion string
-
# :pserver:anonymous@foo.bar:/path => /path
-
# :ext:cvsservername:/path => /path
-
1
def root_url_path
-
root_url.to_s.gsub(/^:.+:\d*/, '')
-
end
-
-
# convert a date/time into the CVS-format
-
1
def time_to_cvstime(time)
-
return nil if time.nil?
-
time = Time.now if time == 'HEAD'
-
-
unless time.kind_of? Time
-
time = Time.parse(time)
-
end
-
return time_to_cvstime_rlog(time)
-
end
-
-
1
def time_to_cvstime_rlog(time)
-
return nil if time.nil?
-
t1 = time.clone.localtime
-
return t1.strftime("%Y-%m-%d %H:%M:%S")
-
end
-
-
1
def normalize_cvs_path(path)
-
normalize_path(path.gsub(/Attic\//,''))
-
end
-
-
1
def normalize_path(path)
-
path.sub(/^(\/)*(.*)/,'\2').sub(/(.*)(,v)+/,'\1')
-
end
-
-
1
def path_with_proj(path)
-
"#{url}#{with_leading_slash(path)}"
-
end
-
1
private :path_with_proj
-
-
1
class Revision < Redmine::Scm::Adapters::Revision
-
# Returns the readable identifier
-
1
def format_identifier
-
revision.to_s
-
end
-
end
-
-
1
def scm_cmd(*args, &block)
-
full_args = ['-d', root_url]
-
full_args += args
-
full_args_locale = []
-
full_args.map do |e|
-
full_args_locale << scm_iconv(@path_encoding, 'UTF-8', e)
-
end
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args_locale.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise ScmCommandAborted, "cvs exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :scm_cmd
-
end
-
-
1
class CvsRevisionHelper
-
1
attr_accessor :complete_rev, :revision, :base, :branchid
-
-
1
def initialize(complete_rev)
-
@complete_rev = complete_rev
-
parseRevision()
-
end
-
-
1
def branchPoint
-
return @base
-
end
-
-
1
def branchVersion
-
if isBranchRevision
-
return @base+"."+@branchid
-
end
-
return @base
-
end
-
-
1
def isBranchRevision
-
!@branchid.nil?
-
end
-
-
1
def prevRev
-
unless @revision == 0
-
return buildRevision( @revision - 1 )
-
end
-
return buildRevision( @revision )
-
end
-
-
1
def is_in_branch_with_symbol(branch_symbol)
-
bpieces = branch_symbol.split(".")
-
branch_start = "#{bpieces[0..-3].join(".")}.#{bpieces[-1]}"
-
return ( branchVersion == branch_start )
-
end
-
-
1
private
-
1
def buildRevision(rev)
-
if rev == 0
-
if @branchid.nil?
-
@base + ".0"
-
else
-
@base
-
end
-
elsif @branchid.nil?
-
@base + "." + rev.to_s
-
else
-
@base + "." + @branchid + "." + rev.to_s
-
end
-
end
-
-
# Interpretiert die cvs revisionsnummern wie z.b. 1.14 oder 1.3.0.15
-
1
def parseRevision()
-
pieces = @complete_rev.split(".")
-
@revision = pieces.last.to_i
-
baseSize = 1
-
baseSize += (pieces.size / 2)
-
@base = pieces[0..-baseSize].join(".")
-
if baseSize > 2
-
@branchid = pieces[-2]
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'rexml/document'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class DarcsAdapter < AbstractAdapter
-
# Darcs executable name
-
1
DARCS_BIN = Redmine::Configuration['scm_darcs_command'] || "darcs"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= DARCS_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (darcs_binary_version || [])
-
end
-
-
1
def client_available
-
!client_version.empty?
-
end
-
-
1
def darcs_binary_version
-
darcsversion = darcs_binary_version_from_command_line.dup
-
if darcsversion.respond_to?(:force_encoding)
-
darcsversion.force_encoding('ASCII-8BIT')
-
end
-
if m = darcsversion.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def darcs_binary_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@url = url
-
@root_url = url
-
end
-
-
1
def supports_cat?
-
# cat supported in darcs 2.0.0 and higher
-
self.class.client_version_above?([2, 0, 0])
-
end
-
-
# Get info about the darcs repository
-
1
def info
-
rev = revisions(nil,nil,nil,{:limit => 1})
-
rev ? Info.new({:root_url => @url, :lastrev => rev.last}) : nil
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
path_prefix = (path.blank? ? '' : "#{path}/")
-
if path.blank?
-
path = ( self.class.client_version_above?([2, 2, 0]) ? @url : '.' )
-
end
-
entries = Entries.new
-
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --xml-output"
-
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
-
cmd << " #{shell_quote path}"
-
shellout(cmd) do |io|
-
begin
-
doc = REXML::Document.new(io)
-
if doc.root.name == 'directory'
-
doc.elements.each('directory/*') do |element|
-
next unless ['file', 'directory'].include? element.name
-
entries << entry_from_xml(element, path_prefix)
-
end
-
elsif doc.root.name == 'file'
-
entries << entry_from_xml(doc.root, path_prefix)
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
entries.compact!
-
entries.sort_by_name
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
path = '.' if path.blank?
-
revisions = Revisions.new
-
cmd = "#{self.class.sq_bin} changes --repodir #{shell_quote @url} --xml-output"
-
cmd << " --from-match #{shell_quote("hash #{identifier_from}")}" if identifier_from
-
cmd << " --last #{options[:limit].to_i}" if options[:limit]
-
shellout(cmd) do |io|
-
begin
-
doc = REXML::Document.new(io)
-
doc.elements.each("changelog/patch") do |patch|
-
message = patch.elements['name'].text
-
message << "\n" + patch.elements['comment'].text.gsub(/\*\*\*END OF DESCRIPTION\*\*\*.*\z/m, '') if patch.elements['comment']
-
revisions << Revision.new({:identifier => nil,
-
:author => patch.attributes['author'],
-
:scmid => patch.attributes['hash'],
-
:time => Time.parse(patch.attributes['local_date']),
-
:message => message,
-
:paths => (options[:with_path] ? get_paths_for_patch(patch.attributes['hash']) : nil)
-
})
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
revisions
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path = '*' if path.blank?
-
cmd = "#{self.class.sq_bin} diff --repodir #{shell_quote @url}"
-
if identifier_to.nil?
-
cmd << " --match #{shell_quote("hash #{identifier_from}")}"
-
else
-
cmd << " --to-match #{shell_quote("hash #{identifier_from}")}"
-
cmd << " --from-match #{shell_quote("hash #{identifier_to}")}"
-
end
-
cmd << " -u #{shell_quote path}"
-
diff = []
-
shellout(cmd) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
diff
-
end
-
-
1
def cat(path, identifier=nil)
-
cmd = "#{self.class.sq_bin} show content --repodir #{shell_quote @url}"
-
cmd << " --match #{shell_quote("hash #{identifier}")}" if identifier
-
cmd << " #{shell_quote path}"
-
cat = nil
-
shellout(cmd) do |io|
-
io.binmode
-
cat = io.read
-
end
-
return nil if $? && $?.exitstatus != 0
-
cat
-
end
-
-
1
private
-
-
# Returns an Entry from the given XML element
-
# or nil if the entry was deleted
-
1
def entry_from_xml(element, path_prefix)
-
modified_element = element.elements['modified']
-
if modified_element.elements['modified_how'].text.match(/removed/)
-
return nil
-
end
-
-
Entry.new({:name => element.attributes['name'],
-
:path => path_prefix + element.attributes['name'],
-
:kind => element.name == 'file' ? 'file' : 'dir',
-
:size => nil,
-
:lastrev => Revision.new({
-
:identifier => nil,
-
:scmid => modified_element.elements['patch'].attributes['hash']
-
})
-
})
-
end
-
-
1
def get_paths_for_patch(hash)
-
paths = get_paths_for_patch_raw(hash)
-
if self.class.client_version_above?([2, 4])
-
orig_paths = paths
-
paths = []
-
add_paths = []
-
add_paths_name = []
-
mod_paths = []
-
other_paths = []
-
orig_paths.each do |path|
-
if path[:action] == 'A'
-
add_paths << path
-
add_paths_name << path[:path]
-
elsif path[:action] == 'M'
-
mod_paths << path
-
else
-
other_paths << path
-
end
-
end
-
add_paths_name.each do |add_path|
-
mod_paths.delete_if { |m| m[:path] == add_path }
-
end
-
paths.concat add_paths
-
paths.concat mod_paths
-
paths.concat other_paths
-
end
-
paths
-
end
-
-
# Retrieve changed paths for a single patch
-
1
def get_paths_for_patch_raw(hash)
-
cmd = "#{self.class.sq_bin} annotate --repodir #{shell_quote @url} --summary --xml-output"
-
cmd << " --match #{shell_quote("hash #{hash}")} "
-
paths = []
-
shellout(cmd) do |io|
-
begin
-
# Darcs xml output has multiple root elements in this case (tested with darcs 1.0.7)
-
# A root element is added so that REXML doesn't raise an error
-
doc = REXML::Document.new("<fake_root>" + io.read + "</fake_root>")
-
doc.elements.each('fake_root/summary/*') do |modif|
-
paths << {:action => modif.name[0,1].upcase,
-
:path => "/" + modif.text.chomp.gsub(/^\s*/, '')
-
}
-
end
-
rescue
-
end
-
end
-
paths
-
rescue CommandFailed
-
paths
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# FileSystem adapter
-
# File written by Paul Rivier, at Demotera.
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'find'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class FilesystemAdapter < AbstractAdapter
-
-
1
class << self
-
1
def client_available
-
true
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil,
-
path_encoding=nil)
-
@url = with_trailling_slash(url)
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def format_path_ends(path, leading=true, trailling=true)
-
path = leading ? with_leading_slash(path) :
-
without_leading_slash(path)
-
trailling ? with_trailling_slash(path) :
-
without_trailling_slash(path)
-
end
-
-
1
def info
-
info = Info.new({:root_url => target(),
-
:lastrev => nil
-
})
-
info
-
rescue CommandFailed
-
return nil
-
end
-
-
1
def entries(path="", identifier=nil, options={})
-
entries = Entries.new
-
trgt_utf8 = target(path)
-
trgt = scm_iconv(@path_encoding, 'UTF-8', trgt_utf8)
-
Dir.new(trgt).each do |e1|
-
e_utf8 = scm_iconv('UTF-8', @path_encoding, e1)
-
next if e_utf8.blank?
-
relative_path_utf8 = format_path_ends(
-
(format_path_ends(path,false,true) + e_utf8),false,false)
-
t1_utf8 = target(relative_path_utf8)
-
t1 = scm_iconv(@path_encoding, 'UTF-8', t1_utf8)
-
relative_path = scm_iconv(@path_encoding, 'UTF-8', relative_path_utf8)
-
e1 = scm_iconv(@path_encoding, 'UTF-8', e_utf8)
-
if File.exist?(t1) and # paranoid test
-
%w{file directory}.include?(File.ftype(t1)) and # avoid special types
-
not File.basename(e1).match(/^\.+$/) # avoid . and ..
-
p1 = File.readable?(t1) ? relative_path : ""
-
utf_8_path = scm_iconv('UTF-8', @path_encoding, p1)
-
entries <<
-
Entry.new({ :name => scm_iconv('UTF-8', @path_encoding, File.basename(e1)),
-
# below : list unreadable files, but dont link them.
-
:path => utf_8_path,
-
:kind => (File.directory?(t1) ? 'dir' : 'file'),
-
:size => (File.directory?(t1) ? nil : [File.size(t1)].pack('l').unpack('L').first),
-
:lastrev =>
-
Revision.new({:time => (File.mtime(t1)) })
-
})
-
end
-
end
-
entries.sort_by_name
-
rescue => err
-
logger.error "scm: filesystem: error: #{err.message}"
-
raise CommandFailed.new(err.message)
-
end
-
-
1
def cat(path, identifier=nil)
-
p = scm_iconv(@path_encoding, 'UTF-8', target(path))
-
File.new(p, "rb").read
-
rescue => err
-
logger.error "scm: filesystem: error: #{err.message}"
-
raise CommandFailed.new(err.message)
-
end
-
-
1
private
-
-
# AbstractAdapter::target is implicitly made to quote paths.
-
# Here we do not shell-out, so we do not want quotes.
-
1
def target(path=nil)
-
# Prevent the use of ..
-
if path and !path.match(/(^|\/)\.\.(\/|$)/)
-
return "#{self.url}#{without_leading_slash(path)}"
-
end
-
return self.url
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class GitAdapter < AbstractAdapter
-
-
# Git executable name
-
1
GIT_BIN = Redmine::Configuration['scm_git_command'] || "git"
-
-
1
class GitBranch < Branch
-
1
attr_accessor :is_default
-
end
-
-
1
class << self
-
1
def client_command
-
@@bin ||= GIT_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (scm_command_version || [])
-
end
-
-
1
def client_available
-
!client_version.empty?
-
end
-
-
1
def scm_command_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version --no-color") { |io| io.read }.to_s
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
-
super
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def info
-
begin
-
Info.new(:root_url => url, :lastrev => lastrev('',nil))
-
rescue
-
nil
-
end
-
end
-
-
1
def branches
-
return @branches if @branches
-
@branches = []
-
cmd_args = %w|branch --no-color --verbose --no-abbrev|
-
git_cmd(cmd_args) do |io|
-
io.each_line do |line|
-
branch_rev = line.match('\s*(\*?)\s*(.*?)\s*([0-9a-f]{40}).*$')
-
bran = GitBranch.new(branch_rev[2])
-
bran.revision = branch_rev[3]
-
bran.scmid = branch_rev[3]
-
bran.is_default = ( branch_rev[1] == '*' )
-
@branches << bran
-
end
-
end
-
@branches.sort!
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def tags
-
return @tags if @tags
-
cmd_args = %w|tag|
-
git_cmd(cmd_args) do |io|
-
@tags = io.readlines.sort!.map{|t| t.strip}
-
end
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def default_branch
-
bras = self.branches
-
return nil if bras.nil?
-
default_bras = bras.select{|x| x.is_default == true}
-
return default_bras.first.to_s if ! default_bras.empty?
-
master_bras = bras.select{|x| x.to_s == 'master'}
-
master_bras.empty? ? bras.first.to_s : 'master'
-
end
-
-
1
def entry(path=nil, identifier=nil)
-
parts = path.to_s.split(%r{[\/\\]}).select {|n| !n.blank?}
-
search_path = parts[0..-2].join('/')
-
search_name = parts[-1]
-
if search_path.blank? && search_name.blank?
-
# Root entry
-
Entry.new(:path => '', :kind => 'dir')
-
else
-
# Search for the entry in the parent directory
-
es = entries(search_path, identifier,
-
options = {:report_last_commit => false})
-
es ? es.detect {|e| e.name == search_name} : nil
-
end
-
end
-
-
1
def entries(path=nil, identifier=nil, options={})
-
path ||= ''
-
p = scm_iconv(@path_encoding, 'UTF-8', path)
-
entries = Entries.new
-
cmd_args = %w|ls-tree -l|
-
cmd_args << "HEAD:#{p}" if identifier.nil?
-
cmd_args << "#{identifier}:#{p}" if identifier
-
git_cmd(cmd_args) do |io|
-
io.each_line do |line|
-
e = line.chomp.to_s
-
if e =~ /^\d+\s+(\w+)\s+([0-9a-f]{40})\s+([0-9-]+)\t(.+)$/
-
type = $1
-
sha = $2
-
size = $3
-
name = $4
-
if name.respond_to?(:force_encoding)
-
name.force_encoding(@path_encoding)
-
end
-
full_path = p.empty? ? name : "#{p}/#{name}"
-
n = scm_iconv('UTF-8', @path_encoding, name)
-
full_p = scm_iconv('UTF-8', @path_encoding, full_path)
-
entries << Entry.new({:name => n,
-
:path => full_p,
-
:kind => (type == "tree") ? 'dir' : 'file',
-
:size => (type == "tree") ? nil : size,
-
:lastrev => options[:report_last_commit] ?
-
lastrev(full_path, identifier) : Revision.new
-
}) unless entries.detect{|entry| entry.name == name}
-
end
-
end
-
end
-
entries.sort_by_name
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def lastrev(path, rev)
-
return nil if path.nil?
-
cmd_args = %w|log --no-color --encoding=UTF-8 --date=iso --pretty=fuller --no-merges -n 1|
-
cmd_args << rev if rev
-
cmd_args << "--" << path unless path.empty?
-
lines = []
-
git_cmd(cmd_args) { |io| lines = io.readlines }
-
begin
-
id = lines[0].split[1]
-
author = lines[1].match('Author:\s+(.*)$')[1]
-
time = Time.parse(lines[4].match('CommitDate:\s+(.*)$')[1])
-
-
Revision.new({
-
:identifier => id,
-
:scmid => id,
-
:author => author,
-
:time => time,
-
:message => nil,
-
:paths => nil
-
})
-
rescue NoMethodError => e
-
logger.error("The revision '#{path}' has a wrong format")
-
return nil
-
end
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def revisions(path, identifier_from, identifier_to, options={})
-
revs = Revisions.new
-
cmd_args = %w|log --no-color --encoding=UTF-8 --raw --date=iso --pretty=fuller --parents --stdin|
-
cmd_args << "--reverse" if options[:reverse]
-
cmd_args << "-n" << "#{options[:limit].to_i}" if options[:limit]
-
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) if path && !path.empty?
-
revisions = []
-
if identifier_from || identifier_to
-
revisions << ""
-
revisions[0] << "#{identifier_from}.." if identifier_from
-
revisions[0] << "#{identifier_to}" if identifier_to
-
else
-
unless options[:includes].blank?
-
revisions += options[:includes]
-
end
-
unless options[:excludes].blank?
-
revisions += options[:excludes].map{|r| "^#{r}"}
-
end
-
end
-
-
git_cmd(cmd_args, {:write_stdin => true}) do |io|
-
io.binmode
-
io.puts(revisions.join("\n"))
-
io.close_write
-
files=[]
-
changeset = {}
-
parsing_descr = 0 #0: not parsing desc or files, 1: parsing desc, 2: parsing files
-
-
io.each_line do |line|
-
if line =~ /^commit ([0-9a-f]{40})(( [0-9a-f]{40})*)$/
-
key = "commit"
-
value = $1
-
parents_str = $2
-
if (parsing_descr == 1 || parsing_descr == 2)
-
parsing_descr = 0
-
revision = Revision.new({
-
:identifier => changeset[:commit],
-
:scmid => changeset[:commit],
-
:author => changeset[:author],
-
:time => Time.parse(changeset[:date]),
-
:message => changeset[:description],
-
:paths => files,
-
:parents => changeset[:parents]
-
})
-
if block_given?
-
yield revision
-
else
-
revs << revision
-
end
-
changeset = {}
-
files = []
-
end
-
changeset[:commit] = $1
-
unless parents_str.nil? or parents_str == ""
-
changeset[:parents] = parents_str.strip.split(' ')
-
end
-
elsif (parsing_descr == 0) && line =~ /^(\w+):\s*(.*)$/
-
key = $1
-
value = $2
-
if key == "Author"
-
changeset[:author] = value
-
elsif key == "CommitDate"
-
changeset[:date] = value
-
end
-
elsif (parsing_descr == 0) && line.chomp.to_s == ""
-
parsing_descr = 1
-
changeset[:description] = ""
-
elsif (parsing_descr == 1 || parsing_descr == 2) \
-
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\t(.+)$/
-
parsing_descr = 2
-
fileaction = $1
-
filepath = $2
-
p = scm_iconv('UTF-8', @path_encoding, filepath)
-
files << {:action => fileaction, :path => p}
-
elsif (parsing_descr == 1 || parsing_descr == 2) \
-
&& line =~ /^:\d+\s+\d+\s+[0-9a-f.]+\s+[0-9a-f.]+\s+(\w)\d+\s+(\S+)\t(.+)$/
-
parsing_descr = 2
-
fileaction = $1
-
filepath = $3
-
p = scm_iconv('UTF-8', @path_encoding, filepath)
-
files << {:action => fileaction, :path => p}
-
elsif (parsing_descr == 1) && line.chomp.to_s == ""
-
parsing_descr = 2
-
elsif (parsing_descr == 1)
-
changeset[:description] << line[4..-1]
-
end
-
end
-
-
if changeset[:commit]
-
revision = Revision.new({
-
:identifier => changeset[:commit],
-
:scmid => changeset[:commit],
-
:author => changeset[:author],
-
:time => Time.parse(changeset[:date]),
-
:message => changeset[:description],
-
:paths => files,
-
:parents => changeset[:parents]
-
})
-
if block_given?
-
yield revision
-
else
-
revs << revision
-
end
-
end
-
end
-
revs
-
rescue ScmCommandAborted => e
-
err_msg = "git log error: #{e.message}"
-
logger.error(err_msg)
-
if block_given?
-
raise CommandFailed, err_msg
-
else
-
revs
-
end
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path ||= ''
-
cmd_args = []
-
if identifier_to
-
cmd_args << "diff" << "--no-color" << identifier_to << identifier_from
-
else
-
cmd_args << "show" << "--no-color" << identifier_from
-
end
-
cmd_args << "--" << scm_iconv(@path_encoding, 'UTF-8', path) unless path.empty?
-
diff = []
-
git_cmd(cmd_args) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def annotate(path, identifier=nil)
-
identifier = 'HEAD' if identifier.blank?
-
cmd_args = %w|blame|
-
cmd_args << "-p" << identifier << "--" << scm_iconv(@path_encoding, 'UTF-8', path)
-
blame = Annotate.new
-
content = nil
-
git_cmd(cmd_args) { |io| io.binmode; content = io.read }
-
# git annotates binary files
-
return nil if content.is_binary_data?
-
identifier = ''
-
# git shows commit author on the first occurrence only
-
authors_by_commit = {}
-
content.split("\n").each do |line|
-
if line =~ /^([0-9a-f]{39,40})\s.*/
-
identifier = $1
-
elsif line =~ /^author (.+)/
-
authors_by_commit[identifier] = $1.strip
-
elsif line =~ /^\t(.*)/
-
blame.add_line($1, Revision.new(
-
:identifier => identifier,
-
:revision => identifier,
-
:scmid => identifier,
-
:author => authors_by_commit[identifier]
-
))
-
identifier = ''
-
author = ''
-
end
-
end
-
blame
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
def cat(path, identifier=nil)
-
if identifier.nil?
-
identifier = 'HEAD'
-
end
-
cmd_args = %w|show --no-color|
-
cmd_args << "#{identifier}:#{scm_iconv(@path_encoding, 'UTF-8', path)}"
-
cat = nil
-
git_cmd(cmd_args) do |io|
-
io.binmode
-
cat = io.read
-
end
-
cat
-
rescue ScmCommandAborted
-
nil
-
end
-
-
1
class Revision < Redmine::Scm::Adapters::Revision
-
# Returns the readable identifier
-
1
def format_identifier
-
identifier[0,8]
-
end
-
end
-
-
1
def git_cmd(args, options = {}, &block)
-
repo_path = root_url || url
-
full_args = ['--git-dir', repo_path]
-
if self.class.client_version_above?([1, 7, 2])
-
full_args << '-c' << 'core.quotepath=false'
-
full_args << '-c' << 'log.decorate=no'
-
end
-
full_args += args
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
-
options,
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise ScmCommandAborted, "git exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :git_cmd
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'cgi'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class MercurialAdapter < AbstractAdapter
-
-
# Mercurial executable name
-
1
HG_BIN = Redmine::Configuration['scm_mercurial_command'] || "hg"
-
1
HELPERS_DIR = File.dirname(__FILE__) + "/mercurial"
-
1
HG_HELPER_EXT = "#{HELPERS_DIR}/redminehelper.py"
-
1
TEMPLATE_NAME = "hg-template"
-
1
TEMPLATE_EXTENSION = "tmpl"
-
-
# raised if hg command exited with error, e.g. unknown revision.
-
1
class HgCommandAborted < CommandFailed; end
-
-
1
class << self
-
1
def client_command
-
@@bin ||= HG_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (hgversion || [])
-
end
-
-
1
def client_available
-
client_version_above?([1, 2])
-
end
-
-
1
def hgversion
-
# The hg version is expressed either as a
-
# release number (eg 0.9.5 or 1.0) or as a revision
-
# id composed of 12 hexa characters.
-
theversion = hgversion_from_command_line.dup
-
if theversion.respond_to?(:force_encoding)
-
theversion.force_encoding('ASCII-8BIT')
-
end
-
if m = theversion.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def hgversion_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
-
1
def template_path
-
@@template_path ||= template_path_for(client_version)
-
end
-
-
1
def template_path_for(version)
-
"#{HELPERS_DIR}/#{TEMPLATE_NAME}-1.0.#{TEMPLATE_EXTENSION}"
-
end
-
end
-
-
1
def initialize(url, root_url=nil, login=nil, password=nil, path_encoding=nil)
-
super
-
@path_encoding = path_encoding.blank? ? 'UTF-8' : path_encoding
-
end
-
-
1
def path_encoding
-
@path_encoding
-
end
-
-
1
def info
-
tip = summary['repository']['tip']
-
Info.new(:root_url => CGI.unescape(summary['repository']['root']),
-
:lastrev => Revision.new(:revision => tip['revision'],
-
:scmid => tip['node']))
-
# rescue HgCommandAborted
-
rescue Exception => e
-
logger.error "hg: error during getting info: #{e.message}"
-
nil
-
end
-
-
1
def tags
-
as_ary(summary['repository']['tag']).map { |e| e['name'] }
-
end
-
-
# Returns map of {'tag' => 'nodeid', ...}
-
1
def tagmap
-
alist = as_ary(summary['repository']['tag']).map do |e|
-
e.values_at('name', 'node')
-
end
-
Hash[*alist.flatten]
-
end
-
-
1
def branches
-
brs = []
-
as_ary(summary['repository']['branch']).each do |e|
-
br = Branch.new(e['name'])
-
br.revision = e['revision']
-
br.scmid = e['node']
-
brs << br
-
end
-
brs
-
end
-
-
# Returns map of {'branch' => 'nodeid', ...}
-
1
def branchmap
-
alist = as_ary(summary['repository']['branch']).map do |e|
-
e.values_at('name', 'node')
-
end
-
Hash[*alist.flatten]
-
end
-
-
1
def summary
-
return @summary if @summary
-
hg 'rhsummary' do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
@summary = parse_xml(output)['rhsummary']
-
rescue
-
end
-
end
-
end
-
1
private :summary
-
-
1
def entries(path=nil, identifier=nil, options={})
-
p1 = scm_iconv(@path_encoding, 'UTF-8', path)
-
manifest = hg('rhmanifest', '-r', CGI.escape(hgrev(identifier)),
-
CGI.escape(without_leading_slash(p1.to_s))) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
parse_xml(output)['rhmanifest']['repository']['manifest']
-
rescue
-
end
-
end
-
path_prefix = path.blank? ? '' : with_trailling_slash(path)
-
-
entries = Entries.new
-
as_ary(manifest['dir']).each do |e|
-
n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
-
p = "#{path_prefix}#{n}"
-
entries << Entry.new(:name => n, :path => p, :kind => 'dir')
-
end
-
-
as_ary(manifest['file']).each do |e|
-
n = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['name']))
-
p = "#{path_prefix}#{n}"
-
lr = Revision.new(:revision => e['revision'], :scmid => e['node'],
-
:identifier => e['node'],
-
:time => Time.at(e['time'].to_i))
-
entries << Entry.new(:name => n, :path => p, :kind => 'file',
-
:size => e['size'].to_i, :lastrev => lr)
-
end
-
-
entries
-
rescue HgCommandAborted
-
nil # means not found
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
revs = Revisions.new
-
each_revision(path, identifier_from, identifier_to, options) { |e| revs << e }
-
revs
-
end
-
-
# Iterates the revisions by using a template file that
-
# makes Mercurial produce a xml output.
-
1
def each_revision(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
hg_args = ['log', '--debug', '-C', '--style', self.class.template_path]
-
hg_args << '-r' << "#{hgrev(identifier_from)}:#{hgrev(identifier_to)}"
-
hg_args << '--limit' << options[:limit] if options[:limit]
-
hg_args << hgtarget(path) unless path.blank?
-
log = hg(*hg_args) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
# Mercurial < 1.5 does not support footer template for '</log>'
-
parse_xml("#{output}</log>")['log']
-
rescue
-
end
-
end
-
as_ary(log['logentry']).each do |le|
-
cpalist = as_ary(le['paths']['path-copied']).map do |e|
-
[e['__content__'], e['copyfrom-path']].map do |s|
-
scm_iconv('UTF-8', @path_encoding, CGI.unescape(s))
-
end
-
end
-
cpmap = Hash[*cpalist.flatten]
-
paths = as_ary(le['paths']['path']).map do |e|
-
p = scm_iconv('UTF-8', @path_encoding, CGI.unescape(e['__content__']) )
-
{:action => e['action'],
-
:path => with_leading_slash(p),
-
:from_path => (cpmap.member?(p) ? with_leading_slash(cpmap[p]) : nil),
-
:from_revision => (cpmap.member?(p) ? le['node'] : nil)}
-
end.sort { |a, b| a[:path] <=> b[:path] }
-
parents_ary = []
-
as_ary(le['parents']['parent']).map do |par|
-
parents_ary << par['__content__'] if par['__content__'] != "000000000000"
-
end
-
yield Revision.new(:revision => le['revision'],
-
:scmid => le['node'],
-
:author => (le['author']['__content__'] rescue ''),
-
:time => Time.parse(le['date']['__content__']),
-
:message => le['msg']['__content__'],
-
:paths => paths,
-
:parents => parents_ary)
-
end
-
self
-
end
-
-
# Returns list of nodes in the specified branch
-
1
def nodes_in_branch(branch, options={})
-
hg_args = ['rhlog', '--template', '{node|short}\n', '--rhbranch', CGI.escape(branch)]
-
hg_args << '--from' << CGI.escape(branch)
-
hg_args << '--to' << '0'
-
hg_args << '--limit' << options[:limit] if options[:limit]
-
hg(*hg_args) { |io| io.readlines.map { |e| e.chomp } }
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
hg_args = %w|rhdiff|
-
if identifier_to
-
hg_args << '-r' << hgrev(identifier_to) << '-r' << hgrev(identifier_from)
-
else
-
hg_args << '-c' << hgrev(identifier_from)
-
end
-
unless path.blank?
-
p = scm_iconv(@path_encoding, 'UTF-8', path)
-
hg_args << CGI.escape(hgtarget(p))
-
end
-
diff = []
-
hg *hg_args do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
diff
-
rescue HgCommandAborted
-
nil # means not found
-
end
-
-
1
def cat(path, identifier=nil)
-
p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
-
hg 'rhcat', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
-
io.binmode
-
io.read
-
end
-
rescue HgCommandAborted
-
nil # means not found
-
end
-
-
1
def annotate(path, identifier=nil)
-
p = CGI.escape(scm_iconv(@path_encoding, 'UTF-8', path))
-
blame = Annotate.new
-
hg 'rhannotate', '-ncu', '-r', CGI.escape(hgrev(identifier)), hgtarget(p) do |io|
-
io.each_line do |line|
-
line.force_encoding('ASCII-8BIT') if line.respond_to?(:force_encoding)
-
next unless line =~ %r{^([^:]+)\s(\d+)\s([0-9a-f]+):\s(.*)$}
-
r = Revision.new(:author => $1.strip, :revision => $2, :scmid => $3,
-
:identifier => $3)
-
blame.add_line($4.rstrip, r)
-
end
-
end
-
blame
-
rescue HgCommandAborted
-
# means not found or cannot be annotated
-
Annotate.new
-
end
-
-
1
class Revision < Redmine::Scm::Adapters::Revision
-
# Returns the readable identifier
-
1
def format_identifier
-
"#{revision}:#{scmid}"
-
end
-
end
-
-
# Runs 'hg' command with the given args
-
1
def hg(*args, &block)
-
repo_path = root_url || url
-
full_args = ['-R', repo_path, '--encoding', 'utf-8']
-
full_args << '--config' << "extensions.redminehelper=#{HG_HELPER_EXT}"
-
full_args << '--config' << 'diff.git=false'
-
full_args += args
-
ret = shellout(
-
self.class.sq_bin + ' ' + full_args.map { |e| shell_quote e.to_s }.join(' '),
-
&block
-
)
-
if $? && $?.exitstatus != 0
-
raise HgCommandAborted, "hg exited with non-zero status: #{$?.exitstatus}"
-
end
-
ret
-
end
-
1
private :hg
-
-
# Returns correct revision identifier
-
1
def hgrev(identifier, sq=false)
-
rev = identifier.blank? ? 'tip' : identifier.to_s
-
rev = shell_quote(rev) if sq
-
rev
-
end
-
1
private :hgrev
-
-
1
def hgtarget(path)
-
path ||= ''
-
root_url + '/' + without_leading_slash(path)
-
end
-
1
private :hgtarget
-
-
1
def as_ary(o)
-
return [] unless o
-
o.is_a?(Array) ? o : Array[o]
-
end
-
1
private :as_ary
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redmine/scm/adapters/abstract_adapter'
-
1
require 'uri'
-
-
1
module Redmine
-
1
module Scm
-
1
module Adapters
-
1
class SubversionAdapter < AbstractAdapter
-
-
# SVN executable name
-
1
SVN_BIN = Redmine::Configuration['scm_subversion_command'] || "svn"
-
-
1
class << self
-
1
def client_command
-
@@bin ||= SVN_BIN
-
end
-
-
1
def sq_bin
-
@@sq_bin ||= shell_quote_command
-
end
-
-
1
def client_version
-
@@client_version ||= (svn_binary_version || [])
-
end
-
-
1
def client_available
-
# --xml options are introduced in 1.3.
-
# http://subversion.apache.org/docs/release-notes/1.3.html
-
client_version_above?([1, 3])
-
end
-
-
1
def svn_binary_version
-
scm_version = scm_version_from_command_line.dup
-
if scm_version.respond_to?(:force_encoding)
-
scm_version.force_encoding('ASCII-8BIT')
-
end
-
if m = scm_version.match(%r{\A(.*?)((\d+\.)+\d+)})
-
m[2].scan(%r{\d+}).collect(&:to_i)
-
end
-
end
-
-
1
def scm_version_from_command_line
-
shellout("#{sq_bin} --version") { |io| io.read }.to_s
-
end
-
end
-
-
# Get info about the svn repository
-
1
def info
-
cmd = "#{self.class.sq_bin} info --xml #{target}"
-
cmd << credentials_string
-
info = nil
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
# root_url = doc.elements["info/entry/repository/root"].text
-
info = Info.new({:root_url => doc['info']['entry']['repository']['root']['__content__'],
-
:lastrev => Revision.new({
-
:identifier => doc['info']['entry']['commit']['revision'],
-
:time => Time.parse(doc['info']['entry']['commit']['date']['__content__']).localtime,
-
:author => (doc['info']['entry']['commit']['author'] ? doc['info']['entry']['commit']['author']['__content__'] : "")
-
})
-
})
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
info
-
rescue CommandFailed
-
return nil
-
end
-
-
# Returns an Entries collection
-
# or nil if the given path doesn't exist in the repository
-
1
def entries(path=nil, identifier=nil, options={})
-
path ||= ''
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
entries = Entries.new
-
cmd = "#{self.class.sq_bin} list --xml #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
each_xml_element(doc['lists']['list'], 'entry') do |entry|
-
commit = entry['commit']
-
commit_date = commit['date']
-
# Skip directory if there is no commit date (usually that
-
# means that we don't have read access to it)
-
next if entry['kind'] == 'dir' && commit_date.nil?
-
name = entry['name']['__content__']
-
entries << Entry.new({:name => URI.unescape(name),
-
:path => ((path.empty? ? "" : "#{path}/") + name),
-
:kind => entry['kind'],
-
:size => ((s = entry['size']) ? s['__content__'].to_i : nil),
-
:lastrev => Revision.new({
-
:identifier => commit['revision'],
-
:time => Time.parse(commit_date['__content__'].to_s).localtime,
-
:author => ((a = commit['author']) ? a['__content__'] : nil)
-
})
-
})
-
end
-
rescue Exception => e
-
logger.error("Error parsing svn output: #{e.message}")
-
logger.error("Output was:\n #{output}")
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
logger.debug("Found #{entries.size} entries in the repository for #{target(path)}") if logger && logger.debug?
-
entries.sort_by_name
-
end
-
-
1
def properties(path, identifier=nil)
-
# proplist xml output supported in svn 1.5.0 and higher
-
return nil unless self.class.client_version_above?([1, 5, 0])
-
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
cmd = "#{self.class.sq_bin} proplist --verbose --xml #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
properties = {}
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
each_xml_element(doc['properties']['target'], 'property') do |property|
-
properties[ property['name'] ] = property['__content__'].to_s
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
properties
-
end
-
-
1
def revisions(path=nil, identifier_from=nil, identifier_to=nil, options={})
-
path ||= ''
-
identifier_from = (identifier_from && identifier_from.to_i > 0) ? identifier_from.to_i : "HEAD"
-
identifier_to = (identifier_to && identifier_to.to_i > 0) ? identifier_to.to_i : 1
-
revisions = Revisions.new
-
cmd = "#{self.class.sq_bin} log --xml -r #{identifier_from}:#{identifier_to}"
-
cmd << credentials_string
-
cmd << " --verbose " if options[:with_paths]
-
cmd << " --limit #{options[:limit].to_i}" if options[:limit]
-
cmd << ' ' + target(path)
-
shellout(cmd) do |io|
-
output = io.read
-
if output.respond_to?(:force_encoding)
-
output.force_encoding('UTF-8')
-
end
-
begin
-
doc = parse_xml(output)
-
each_xml_element(doc['log'], 'logentry') do |logentry|
-
paths = []
-
each_xml_element(logentry['paths'], 'path') do |path|
-
paths << {:action => path['action'],
-
:path => path['__content__'],
-
:from_path => path['copyfrom-path'],
-
:from_revision => path['copyfrom-rev']
-
}
-
end if logentry['paths'] && logentry['paths']['path']
-
paths.sort! { |x,y| x[:path] <=> y[:path] }
-
-
revisions << Revision.new({:identifier => logentry['revision'],
-
:author => (logentry['author'] ? logentry['author']['__content__'] : ""),
-
:time => Time.parse(logentry['date']['__content__'].to_s).localtime,
-
:message => logentry['msg']['__content__'],
-
:paths => paths
-
})
-
end
-
rescue
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
revisions
-
end
-
-
1
def diff(path, identifier_from, identifier_to=nil)
-
path ||= ''
-
identifier_from = (identifier_from and identifier_from.to_i > 0) ? identifier_from.to_i : ''
-
-
identifier_to = (identifier_to and identifier_to.to_i > 0) ? identifier_to.to_i : (identifier_from.to_i - 1)
-
-
cmd = "#{self.class.sq_bin} diff -r "
-
cmd << "#{identifier_to}:"
-
cmd << "#{identifier_from}"
-
cmd << " #{target(path)}@#{identifier_from}"
-
cmd << credentials_string
-
diff = []
-
shellout(cmd) do |io|
-
io.each_line do |line|
-
diff << line
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
diff
-
end
-
-
1
def cat(path, identifier=nil)
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
cmd = "#{self.class.sq_bin} cat #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
cat = nil
-
shellout(cmd) do |io|
-
io.binmode
-
cat = io.read
-
end
-
return nil if $? && $?.exitstatus != 0
-
cat
-
end
-
-
1
def annotate(path, identifier=nil)
-
identifier = (identifier and identifier.to_i > 0) ? identifier.to_i : "HEAD"
-
cmd = "#{self.class.sq_bin} blame #{target(path)}@#{identifier}"
-
cmd << credentials_string
-
blame = Annotate.new
-
shellout(cmd) do |io|
-
io.each_line do |line|
-
next unless line =~ %r{^\s*(\d+)\s*(\S+)\s(.*)$}
-
rev = $1
-
blame.add_line($3.rstrip,
-
Revision.new(
-
:identifier => rev,
-
:revision => rev,
-
:author => $2.strip
-
))
-
end
-
end
-
return nil if $? && $?.exitstatus != 0
-
blame
-
end
-
-
1
private
-
-
1
def credentials_string
-
str = ''
-
str << " --username #{shell_quote(@login)}" unless @login.blank?
-
str << " --password #{shell_quote(@password)}" unless @login.blank? || @password.blank?
-
str << " --no-auth-cache --non-interactive"
-
str
-
end
-
-
# Helper that iterates over the child elements of a xml node
-
# MiniXml returns a hash when a single child is found
-
# or an array of hashes for multiple children
-
1
def each_xml_element(node, name)
-
if node && node[name]
-
if node[name].is_a?(Hash)
-
yield node[name]
-
else
-
node[name].each do |element|
-
yield element
-
end
-
end
-
end
-
end
-
-
1
def target(path = '')
-
base = path.match(/^\//) ? root_url : url
-
uri = "#{base}/#{path}"
-
uri = URI.escape(URI.escape(uri), '[]')
-
shell_quote(uri.gsub(/[?<>\*]/, ''))
-
end
-
end
-
end
-
end
-
end
-
1
module Redmine
-
1
module Scm
-
1
class Base
-
1
class << self
-
-
1
def all
-
@scms
-
end
-
-
# Add a new SCM adapter and repository
-
1
def add(scm_name)
-
7
@scms ||= []
-
7
@scms << scm_name
-
end
-
-
# Remove a SCM adapter from Redmine's list of supported scms
-
1
def delete(scm_name)
-
@scms.delete(scm_name)
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Search
-
-
1
mattr_accessor :available_search_types
-
-
1
@@available_search_types = []
-
-
1
class << self
-
1
def map(&block)
-
1
yield self
-
end
-
-
# Registers a search provider
-
1
def register(search_type, options={})
-
7
search_type = search_type.to_s
-
7
@@available_search_types << search_type unless @@available_search_types.include?(search_type)
-
end
-
end
-
-
1
module Controller
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
17
@@default_search_scopes = Hash.new {|hash, key| hash[key] = {:default => nil, :actions => {}}}
-
1
mattr_accessor :default_search_scopes
-
-
# Set the default search scope for a controller or specific actions
-
# Examples:
-
# * search_scope :issues # => sets the search scope to :issues for the whole controller
-
# * search_scope :issues, :only => :index
-
# * search_scope :issues, :only => [:index, :show]
-
1
def default_search_scope(id, options = {})
-
8
if actions = options[:only]
-
actions = [] << actions unless actions.is_a?(Array)
-
actions.each {|a| default_search_scopes[controller_name.to_sym][:actions][a.to_sym] = id.to_s}
-
else
-
8
default_search_scopes[controller_name.to_sym][:default] = id.to_s
-
end
-
end
-
end
-
-
1
def default_search_scopes
-
718
self.class.default_search_scopes
-
end
-
-
# Returns the default search scope according to the current action
-
1
def default_search_scope
-
@default_search_scope ||= default_search_scopes[controller_name.to_sym][:actions][action_name.to_sym] ||
-
379
default_search_scopes[controller_name.to_sym][:default]
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module SubclassFactory
-
1
def self.included(base)
-
3
base.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
# Returns an instance of the given subclass name
-
1
def new_subclass_instance(class_name, *args)
-
klass = nil
-
begin
-
klass = class_name.to_s.classify.constantize
-
rescue
-
# invalid class name
-
end
-
unless subclasses.include? klass
-
klass = nil
-
end
-
if klass
-
klass.new(*args)
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Themes
-
-
# Return an array of installed themes
-
1
def self.themes
-
@@installed_themes ||= scan_themes
-
end
-
-
# Rescan themes directory
-
1
def self.rescan
-
@@installed_themes = scan_themes
-
end
-
-
# Return theme for given id, or nil if it's not found
-
1
def self.theme(id, options={})
-
762
return nil if id.blank?
-
-
found = themes.find {|t| t.id == id}
-
if found.nil? && options[:rescan] != false
-
rescan
-
found = theme(id, :rescan => false)
-
end
-
found
-
end
-
-
# Class used to represent a theme
-
1
class Theme
-
1
attr_reader :path, :name, :dir
-
-
1
def initialize(path)
-
@path = path
-
@dir = File.basename(path)
-
@name = @dir.humanize
-
@stylesheets = nil
-
@javascripts = nil
-
end
-
-
# Directory name used as the theme id
-
1
def id; dir end
-
-
1
def ==(theme)
-
theme.is_a?(Theme) && theme.dir == dir
-
end
-
-
1
def <=>(theme)
-
name <=> theme.name
-
end
-
-
1
def stylesheets
-
@stylesheets ||= assets("stylesheets", "css")
-
end
-
-
1
def images
-
@images ||= assets("images")
-
end
-
-
1
def javascripts
-
@javascripts ||= assets("javascripts", "js")
-
end
-
-
1
def stylesheet_path(source)
-
"/themes/#{dir}/stylesheets/#{source}"
-
end
-
-
1
def image_path(source)
-
"/themes/#{dir}/images/#{source}"
-
end
-
-
1
def javascript_path(source)
-
"/themes/#{dir}/javascripts/#{source}"
-
end
-
-
1
private
-
-
1
def assets(dir, ext=nil)
-
if ext
-
Dir.glob("#{path}/#{dir}/*.#{ext}").collect {|f| File.basename(f).gsub(/\.#{ext}$/, '')}
-
else
-
Dir.glob("#{path}/#{dir}/*").collect {|f| File.basename(f)}
-
end
-
end
-
end
-
-
1
private
-
-
1
def self.scan_themes
-
dirs = Dir.glob("#{Rails.public_path}/themes/*").select do |f|
-
# A theme should at least override application.css
-
File.directory?(f) && File.exist?("#{f}/stylesheets/application.css")
-
end
-
dirs.collect {|dir| Theme.new(dir)}.sort
-
end
-
end
-
end
-
-
1
module ApplicationHelper
-
1
def current_theme
-
1441
unless instance_variable_defined?(:@current_theme)
-
403
@current_theme = Redmine::Themes.theme(Setting.ui_theme)
-
end
-
1441
@current_theme
-
end
-
-
# Returns the header tags for the current theme
-
1
def heads_for_theme
-
359
if current_theme && current_theme.javascripts.include?('theme')
-
javascript_include_tag current_theme.javascript_path('theme')
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Utils
-
1
class << self
-
# Returns the relative root url of the application
-
1
def relative_url_root
-
22
ActionController::Base.respond_to?('relative_url_root') ?
-
ActionController::Base.relative_url_root.to_s :
-
ActionController::Base.config.relative_url_root.to_s
-
end
-
-
# Sets the relative root url of the application
-
1
def relative_url_root=(arg)
-
if ActionController::Base.respond_to?('relative_url_root=')
-
ActionController::Base.relative_url_root=arg
-
else
-
ActionController::Base.config.relative_url_root = arg
-
end
-
end
-
-
# Generates a n bytes random hex string
-
# Example:
-
# random_hex(4) # => "89b8c729"
-
1
def random_hex(n)
-
131
SecureRandom.hex(n)
-
end
-
end
-
end
-
end
-
1
require 'rexml/document'
-
-
1
module Redmine
-
1
module VERSION #:nodoc:
-
1
MAJOR = 2
-
1
MINOR = 0
-
1
TINY = 2
-
-
# Branch values:
-
# * official release: nil
-
# * stable branch: stable
-
# * trunk: devel
-
1
BRANCH = 'stable'
-
-
1
def self.revision
-
1
revision = nil
-
1
entries_path = "#{Rails.root}/.svn/entries"
-
1
if File.readable?(entries_path)
-
begin
-
f = File.open(entries_path, 'r')
-
entries = f.read
-
f.close
-
if entries.match(%r{^\d+})
-
revision = $1.to_i if entries.match(%r{^\d+\s+dir\s+(\d+)\s})
-
else
-
xml = REXML::Document.new(entries)
-
revision =
-
xml.elements['wc-entries'].elements[1].attributes['revision'].to_i
-
end
-
rescue
-
# Could not find the current revision
-
end
-
end
-
1
revision
-
end
-
-
1
REVISION = self.revision
-
1
ARRAY = [MAJOR, MINOR, TINY, BRANCH, REVISION].compact
-
1
STRING = ARRAY.join('.')
-
-
3462
def self.to_a; ARRAY end
-
3461
def self.to_s; STRING end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Views
-
1
class ApiTemplateHandler
-
1
def self.call(template)
-
"Redmine::Views::Builders.for(params[:format]) do |api|; #{template.source}; self.output_buffer = api.output; end"
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'action_view/helpers/form_helper'
-
-
1
class Redmine::Views::LabelledFormBuilder < ActionView::Helpers::FormBuilder
-
1
include Redmine::I18n
-
-
(field_helpers.map(&:to_s) - %w(radio_button hidden_field fields_for) +
-
1
%w(date_select)).each do |selector|
-
14
src = <<-END_SRC
-
def #{selector}(field, options = {})
-
label_for_field(field, options) + super(field, options.except(:label)).html_safe
-
end
-
END_SRC
-
14
class_eval src, __FILE__, __LINE__
-
end
-
-
1
def select(field, choices, options = {}, html_options = {})
-
35
label_for_field(field, options) + super(field, choices, options, html_options.except(:label)).html_safe
-
end
-
-
# Returns a label tag for the given field
-
1
def label_for_field(field, options = {})
-
87
return ''.html_safe if options.delete(:no_label)
-
79
text = options[:label].is_a?(Symbol) ? l(options[:label]) : options[:label]
-
79
text ||= l(("field_" + field.to_s.gsub(/\_id$/, "")).to_sym)
-
79
text += @template.content_tag("span", " *", :class => "required") if options.delete(:required)
-
79
@template.content_tag("label", text.html_safe,
-
79
:class => (@object && @object.errors[field].present? ? "error" : nil),
-
79
:for => (@object_name.to_s + "_" + field.to_s))
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Views
-
1
module MyPage
-
1
module Block
-
1
def self.additional_blocks
-
@@additional_blocks ||= Dir.glob("#{Redmine::Plugin.directory}/*/app/views/my/blocks/_*.{rhtml,erb}").inject({}) do |h,file|
-
1
name = File.basename(file).split('.').first.gsub(/^_/, '')
-
1
h[name] = name.to_sym
-
1
h
-
1
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module Views
-
1
class OtherFormatsBuilder
-
1
def initialize(view)
-
25
@view = view
-
end
-
-
1
def link_to(name, options={})
-
62
url = { :format => name.to_s.downcase }.merge(options.delete(:url) || {}).except('page')
-
62
caption = options.delete(:caption) || name
-
62
html_options = { :class => name.to_s.downcase, :rel => 'nofollow' }.merge(options)
-
62
@view.content_tag('span', @view.link_to(caption, url, html_options))
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
class StaleSectionError < Exception; end
-
-
1
@@formatters = {}
-
-
1
class << self
-
1
def map
-
1
yield self
-
end
-
-
1
def register(name, formatter, helper)
-
1
raise ArgumentError, "format name '#{name}' is already taken" if @@formatters[name.to_s]
-
1
@@formatters[name.to_s] = {:formatter => formatter, :helper => helper}
-
end
-
-
1
def formatter
-
formatter_for(Setting.text_formatting)
-
end
-
-
1
def formatter_for(name)
-
2297
entry = @@formatters[name.to_s]
-
2297
(entry && entry[:formatter]) || Redmine::WikiFormatting::NullFormatter::Formatter
-
end
-
-
1
def helper_for(name)
-
4
entry = @@formatters[name.to_s]
-
4
(entry && entry[:helper]) || Redmine::WikiFormatting::NullFormatter::Helper
-
end
-
-
1
def format_names
-
@@formatters.keys.map
-
end
-
-
1
def to_html(format, text, options = {})
-
2297
text = if Setting.cache_formatted_text? && text.size > 2.kilobyte && cache_store && cache_key = cache_key_for(format, options[:object], options[:attribute])
-
# Text retrieved from the cache store may be frozen
-
# We need to dup it so we can do in-place substitutions with gsub!
-
cache_store.fetch cache_key do
-
formatter_for(format).new(text).to_html
-
end.dup
-
else
-
2297
formatter_for(format).new(text).to_html
-
end
-
2297
text
-
end
-
-
# Returns true if the text formatter supports single section edit
-
1
def supports_section_edit?
-
(formatter.instance_methods & ['update_section', :update_section]).any?
-
end
-
-
# Returns a cache key for the given text +format+, +object+ and +attribute+ or nil if no caching should be done
-
1
def cache_key_for(format, object, attribute)
-
if object && attribute && !object.new_record? && object.respond_to?(:updated_on) && !format.blank?
-
"formatted_text/#{format}/#{object.class.model_name.cache_key}/#{object.id}-#{attribute}-#{object.updated_on.to_s(:number)}"
-
end
-
end
-
-
# Returns the cache store used to cache HTML output
-
1
def cache_store
-
ActionController::Base.cache_store
-
end
-
end
-
-
1
module LinksHelper
-
AUTO_LINK_RE = %r{
-
( # leading text
-
<\w+.*?>| # leading HTML tag, or
-
[^=<>!:'"/]| # leading punctuation, or
-
^ # beginning of line
-
)
-
(
-
(?:https?://)| # protocol spec, or
-
(?:s?ftps?://)|
-
(?:www\.) # www.*
-
)
-
(
-
(\S+?) # url
-
(\/)? # slash
-
)
-
((?:>)?|[^\w\=\/;\(\)]*?) # post
-
(?=<|\s|$)
-
1
}x unless const_defined?(:AUTO_LINK_RE)
-
-
# Destructively remplaces urls into clickable links
-
1
def auto_link!(text)
-
2297
text.gsub!(AUTO_LINK_RE) do
-
1112
all, leading, proto, url, post = $&, $1, $2, $3, $6
-
1112
if leading =~ /<a\s/i || leading =~ /![<>=]?/
-
# don't replace URL's that are already linked
-
# and URL's prefixed with ! !> !< != (textile images)
-
all
-
else
-
# Idea below : an URL with unbalanced parethesis and
-
# ending by ')' is put into external parenthesis
-
1112
if ( url[-1]==?) and ((url.count("(") - url.count(")")) < 0 ) )
-
url=url[0..-2] # discard closing parenth from url
-
post = ")"+post # add closing parenth to post
-
end
-
1112
content = proto + url
-
1112
href = "#{proto=="www."?"http://www.":proto}#{url}"
-
1112
%(#{leading}<a class="external" href="#{ERB::Util.html_escape href}">#{ERB::Util.html_escape content}</a>#{post}).html_safe
-
end
-
end
-
end
-
-
# Destructively remplaces email addresses into clickable links
-
1
def auto_mailto!(text)
-
2297
text.gsub!(/([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)/) do
-
mail = $1
-
if text.match(/<a\b[^>]*>(.*)(#{Regexp.escape(mail)})(.*)<\/a>/)
-
mail
-
else
-
%(<a class="email" href="mailto:#{ERB::Util.html_escape mail}">#{ERB::Util.html_escape mail}</a>).html_safe
-
end
-
end
-
end
-
end
-
-
# Default formatter module
-
1
module NullFormatter
-
1
class Formatter
-
1
include ActionView::Helpers::TagHelper
-
1
include ActionView::Helpers::TextHelper
-
1
include ActionView::Helpers::UrlHelper
-
1
include Redmine::WikiFormatting::LinksHelper
-
-
1
def initialize(text)
-
@text = text
-
end
-
-
1
def to_html(*args)
-
t = CGI::escapeHTML(@text)
-
auto_link!(t)
-
auto_mailto!(t)
-
simple_format(t, {}, :sanitize => false)
-
end
-
end
-
-
1
module Helper
-
1
def wikitoolbar_for(field_id)
-
end
-
-
1
def heads_for_wiki_formatter
-
end
-
-
1
def initial_page_content(page)
-
page.pretty_title.to_s
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
module Macros
-
1
module Definitions
-
1
def exec_macro(name, obj, args)
-
method_name = "macro_#{name}"
-
send(method_name, obj, args) if respond_to?(method_name)
-
end
-
-
1
def extract_macro_options(args, *keys)
-
options = {}
-
while args.last.to_s.strip =~ %r{^(.+)\=(.+)$} && keys.include?($1.downcase.to_sym)
-
options[$1.downcase.to_sym] = $2
-
args.pop
-
end
-
return [args, options]
-
end
-
end
-
-
1
@@available_macros = {}
-
-
1
class << self
-
# Called with a block to define additional macros.
-
# Macro blocks accept 2 arguments:
-
# * obj: the object that is rendered
-
# * args: macro arguments
-
#
-
# Plugins can use this method to define new macros:
-
#
-
# Redmine::WikiFormatting::Macros.register do
-
# desc "This is my macro"
-
# macro :my_macro do |obj, args|
-
# "My macro output"
-
# end
-
# end
-
1
def register(&block)
-
class_eval(&block) if block_given?
-
end
-
-
1
private
-
# Defines a new macro with the given name and block.
-
1
def macro(name, &block)
-
4
name = name.to_sym if name.is_a?(String)
-
4
@@available_macros[name] = @@desc || ''
-
4
@@desc = nil
-
4
raise "Can not create a macro without a block!" unless block_given?
-
4
Definitions.send :define_method, "macro_#{name}".downcase, &block
-
end
-
-
# Sets description for the next macro to be defined
-
1
def desc(txt)
-
4
@@desc = txt
-
end
-
end
-
-
# Builtin macros
-
1
desc "Sample macro."
-
1
macro :hello_world do |obj, args|
-
"Hello world! Object: #{obj.class.name}, " + (args.empty? ? "Called with no argument." : "Arguments: #{args.join(', ')}")
-
end
-
-
1
desc "Displays a list of all available macros, including description if available."
-
1
macro :macro_list do |obj, args|
-
out = ''.html_safe
-
@@available_macros.keys.collect(&:to_s).sort.each do |macro|
-
out << content_tag('dt', content_tag('code', macro))
-
out << content_tag('dd', textilizable(@@available_macros[macro.to_sym]))
-
end
-
content_tag('dl', out)
-
end
-
-
desc "Displays a list of child pages. With no argument, it displays the child pages of the current wiki page. Examples:\n\n" +
-
" !{{child_pages}} -- can be used from a wiki page only\n" +
-
" !{{child_pages(Foo)}} -- lists all children of page Foo\n" +
-
1
" !{{child_pages(Foo, parent=1)}} -- same as above with a link to page Foo"
-
1
macro :child_pages do |obj, args|
-
args, options = extract_macro_options(args, :parent)
-
page = nil
-
if args.size > 0
-
page = Wiki.find_page(args.first.to_s, :project => @project)
-
elsif obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)
-
page = obj.page
-
else
-
raise 'With no argument, this macro can be called from wiki pages only.'
-
end
-
raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
-
pages = ([page] + page.descendants).group_by(&:parent_id)
-
render_page_hierarchy(pages, options[:parent] ? page.parent_id : page.id)
-
end
-
-
1
desc "Include a wiki page. Example:\n\n !{{include(Foo)}}\n\nor to include a page of a specific project wiki:\n\n !{{include(projectname:Foo)}}"
-
1
macro :include do |obj, args|
-
page = Wiki.find_page(args.first.to_s, :project => @project)
-
raise 'Page not found' if page.nil? || !User.current.allowed_to?(:view_wiki_pages, page.wiki.project)
-
@included_wiki_pages ||= []
-
raise 'Circular inclusion detected' if @included_wiki_pages.include?(page.title)
-
@included_wiki_pages << page.title
-
out = textilizable(page.content, :text, :attachments => page.attachments, :headings => false)
-
@included_wiki_pages.pop
-
out
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
require 'redcloth3'
-
1
require 'digest/md5'
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
module Textile
-
1
class Formatter < RedCloth3
-
1
include ActionView::Helpers::TagHelper
-
1
include Redmine::WikiFormatting::LinksHelper
-
-
1
alias :inline_auto_link :auto_link!
-
1
alias :inline_auto_mailto :auto_mailto!
-
-
# auto_link rule after textile rules so that it doesn't break !image_url! tags
-
1
RULES = [:textile, :block_markdown_rule, :inline_auto_link, :inline_auto_mailto]
-
-
1
def initialize(*args)
-
2297
super
-
2297
self.hard_breaks=true
-
2297
self.no_span_caps=true
-
2297
self.filter_styles=false
-
end
-
-
1
def to_html(*rules)
-
2297
@toc = []
-
2297
super(*RULES).to_s
-
end
-
-
1
def get_section(index)
-
section = extract_sections(index)[1]
-
hash = Digest::MD5.hexdigest(section)
-
return section, hash
-
end
-
-
1
def update_section(index, update, hash=nil)
-
t = extract_sections(index)
-
if hash.present? && hash != Digest::MD5.hexdigest(t[1])
-
raise Redmine::WikiFormatting::StaleSectionError
-
end
-
t[1] = update unless t[1].blank?
-
t.reject(&:blank?).join "\n\n"
-
end
-
-
1
def extract_sections(index)
-
@pre_list = []
-
text = self.dup
-
rip_offtags text, false, false
-
before = ''
-
s = ''
-
after = ''
-
i = 0
-
l = 1
-
started = false
-
ended = false
-
text.scan(/(((?:.*?)(\A|\r?\n\r?\n))(h(\d+)(#{A}#{C})\.(?::(\S+))? (.*?)$)|.*)/m).each do |all, content, lf, heading, level|
-
if heading.nil?
-
if ended
-
after << all
-
elsif started
-
s << all
-
else
-
before << all
-
end
-
break
-
end
-
i += 1
-
if ended
-
after << all
-
elsif i == index
-
l = level.to_i
-
before << content
-
s << heading
-
started = true
-
elsif i > index
-
s << content
-
if level.to_i > l
-
s << heading
-
else
-
after << heading
-
ended = true
-
end
-
else
-
before << all
-
end
-
end
-
sections = [before.strip, s.strip, after.strip]
-
sections.each {|section| smooth_offtags_without_code_highlighting section}
-
sections
-
end
-
-
1
private
-
-
# Patch for RedCloth. Fixed in RedCloth r128 but _why hasn't released it yet.
-
# <a href="http://code.whytheluckystiff.net/redcloth/changeset/128">http://code.whytheluckystiff.net/redcloth/changeset/128</a>
-
1
def hard_break( text )
-
2297
text.gsub!( /(.)\n(?!\n|\Z| *([#*=]+(\s|$)|[{|]))/, "\\1<br />" ) if hard_breaks
-
end
-
-
1
alias :smooth_offtags_without_code_highlighting :smooth_offtags
-
# Patch to add code highlighting support to RedCloth
-
1
def smooth_offtags( text )
-
2297
unless @pre_list.empty?
-
## replace <pre> content
-
text.gsub!(/<redpre#(\d+)>/) do
-
content = @pre_list[$1.to_i]
-
if content.match(/<code\s+class="(\w+)">\s?(.+)/m)
-
content = "<code class=\"#{$1} syntaxhl\">" +
-
Redmine::SyntaxHighlighting.highlight_by_language($2, $1)
-
end
-
content
-
end
-
end
-
end
-
end
-
end
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2012 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
module Redmine
-
1
module WikiFormatting
-
1
module Textile
-
1
module Helper
-
1
def wikitoolbar_for(field_id)
-
6
heads_for_wiki_formatter
-
# Is there a simple way to link to a public resource?
-
6
url = "#{Redmine::Utils.relative_url_root}/help/wiki_syntax.html"
-
6
help_link = link_to(l(:setting_text_formatting), url,
-
:onclick => "window.open(\"#{ url }\", \"\", \"resizable=yes, location=no, width=300, height=640, menubar=no, status=no, scrollbars=yes\"); return false;")
-
-
6
javascript_tag("var wikiToolbar = new jsToolBar($('#{field_id}')); wikiToolbar.setHelpLink('#{escape_javascript help_link}'); wikiToolbar.draw();")
-
end
-
-
1
def initial_page_content(page)
-
"h1. #{@page.pretty_title}"
-
end
-
-
1
def heads_for_wiki_formatter
-
6
unless @heads_for_wiki_formatter_included
-
4
content_for :header_tags do
-
javascript_include_tag('jstoolbar/jstoolbar') +
-
javascript_include_tag('jstoolbar/textile') +
-
javascript_include_tag("jstoolbar/lang/jstoolbar-#{current_language.to_s.downcase}") +
-
4
stylesheet_link_tag('jstoolbar')
-
end
-
4
@heads_for_wiki_formatter_included = true
-
end
-
end
-
end
-
end
-
end
-
end
-
1
class RbAllProjectsController < ApplicationController
-
1
unloadable
-
-
1
before_filter :authorize_global
-
-
1
def statistics
-
1
@projects = EnabledModule.find(:all,
-
:conditions => ["enabled_modules.name = 'backlogs' and status = ?", Project::STATUS_ACTIVE],
-
:include => :project,
-
1
:joins => :project).collect { |mod| mod.project }
-
1
@projects.sort! {|a, b| a.scrum_statistics.score <=> b.scrum_statistics.score}
-
end
-
-
end
-
# Base class of all controllers in Redmine Backlogs
-
1
class RbApplicationController < ApplicationController
-
1
unloadable
-
-
1
before_filter :load_project, :authorize, :check_if_plugin_is_configured
-
-
1
private
-
-
# Loads the project to be used by the authorize filter to
-
# determine if User.current has permission to invoke the method in question.
-
1
def load_project
-
283
@project = if params[:sprint_id]
-
109
load_sprint
-
109
@sprint.project
-
elsif params[:release_id]
-
7
load_release
-
7
@release.project
-
elsif params[:project_id]
-
167
Project.find(params[:project_id])
-
else
-
raise "Cannot determine project (#{params.inspect})"
-
end
-
end
-
-
1
def check_if_plugin_is_configured
-
370
@settings = Backlogs.settings
-
370
if @settings[:story_trackers].blank? || @settings[:task_tracker].blank?
-
respond_to do |format|
-
format.html { render :file => "shared/not_configured" }
-
end
-
end
-
end
-
-
1
def load_sprint
-
109
@sprint = RbSprint.find(params[:sprint_id])
-
end
-
-
1
def load_release
-
7
@release = RbRelease.find(params[:release_id])
-
end
-
end
-
1
include RbCommonHelper
-
-
1
class RbBurndownChartsController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
3
respond_to do |format|
-
3
format.html
-
end
-
end
-
-
1
def embedded
-
respond_to do |format|
-
format.html { render :template => 'rb_burndown_charts/show.html.erb', :layout => false }
-
end
-
end
-
-
1
def print
-
@width = Backlogs.setting[:burndown_print_width].to_s
-
@height = Backlogs.setting[:burndown_print_height].to_s
-
if @width.blank? || @height.blank?
-
@width = '1300'
-
@height = '600'
-
end
-
respond_to do |format|
-
format.html { render :layout => false }
-
end
-
end
-
-
end
-
1
require 'icalendar'
-
-
1
class RbCalendarsController < RbApplicationController
-
1
unloadable
-
-
1
case Backlogs.platform
-
when :redmine
-
1
before_filter :require_admin_or_api_request, :only => :ical
-
1
accept_api_auth :ical
-
when :chiliproject
-
accept_key_auth :ical
-
end
-
-
1
def ical
-
1
respond_to do |format|
-
2
format.xml { send_data(generate_ical, :disposition => 'attachment') }
-
end
-
end
-
-
1
private
-
-
1
def generate_ical
-
1
cal = Icalendar::Calendar.new
-
-
# current + future sprints
-
1
RbSprint.find(:all, :conditions => ["not sprint_start_date is null and not effective_date is null and project_id = ? and effective_date >= ?", @project.id, Date.today]).each {|sprint|
-
1
summary_text = l(:event_sprint_summary, { :project => @project.name, :summary => sprint.name } )
-
1
description_text = "#{sprint.name}: #{url_for(:controller => 'rb_queries', :only_path => false, :action => 'show', :project_id => @project.id, :sprint_id => sprint.id)}\n#{sprint.description}"
-
-
1
cal.event do
-
1
dtstart sprint.sprint_start_date
-
1
dtend sprint.effective_date
-
1
summary summary_text
-
1
description description_text
-
1
klass 'PRIVATE'
-
1
transp 'TRANSPARENT'
-
end
-
}
-
-
1
open_issues = %Q[
-
#{IssueStatus.table_name}.is_closed = ?
-
and tracker_id in (?)
-
and fixed_version_id in (
-
select id
-
from versions
-
where project_id = ?
-
and status = 'open'
-
and not sprint_start_date is null
-
and effective_date >= ?
-
)
-
]
-
1
open_issues_and_impediments = %Q[
-
(assigned_to_id is null or assigned_to_id = ?)
-
and
-
(
-
(#{open_issues})
-
or
-
( #{IssueStatus.table_name}.is_closed = ?
-
and #{Issue.table_name}.id in (
-
select issue_from_id
-
from issue_relations
-
join issues on issues.id = issue_to_id and relation_type = 'blocks'
-
where #{open_issues})
-
)
-
)
-
]
-
-
1
conditions = [open_issues_and_impediments]
-
# me or none
-
1
conditions << User.current.id
-
-
# open stories/tasks
-
1
conditions << false
-
1
conditions << RbStory.trackers + [RbTask.tracker]
-
1
conditions << @project.id
-
1
conditions << Date.today
-
-
# open impediments...
-
1
conditions << false
-
-
# ... for open stories/tasks
-
1
conditions << false
-
1
conditions << RbStory.trackers + [RbTask.tracker]
-
1
conditions << @project.id
-
1
conditions << Date.today
-
-
1
issues = Issue.find(:all, :include => :status, :conditions => conditions).each {|issue|
-
1
summary_text = l(:todo_issue_summary, { :type => issue.tracker.name, :summary => issue.subject } )
-
1
description_text = "#{issue.subject}: #{url_for(:controller => 'issues', :only_path => false, :action => 'show', :id => issue.id)}\n#{issue.description}"
-
# I know this should be "cal.todo do", but outlook in it's
-
# infinite stupidity doesn't support VTODO
-
1
cal.event do
-
1
summary summary_text
-
1
description description_text
-
1
dtstart Date.today
-
1
dtend (Date.today + 1)
-
1
klass 'PRIVATE'
-
1
transp 'TRANSPARENT'
-
end
-
}
-
-
1
cal.to_ical
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbHooksRenderController < RbApplicationController
-
1
unloadable
-
-
1
def view_issues_sidebar
-
12
locals = {
-
:sprints => RbSprint.open_sprints(@project),
-
:project => @project,
-
:sprint => @sprint,
-
12
:webcal => (request.ssl? ? 'webcals' : 'webcal'),
-
:key => User.current.api_key
-
}
-
-
12
respond_to do |format|
-
24
format.html { render :template => 'backlogs/view_issues_sidebar.html.erb', :layout => false, :locals => locals }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbImpedimentsController < RbApplicationController
-
1
unloadable
-
-
1
def create
-
5
@settings = Backlogs.settings
-
5
begin
-
5
@impediment = RbTask.create_with_relationships(params, User.current.id, @project.id, true)
-
rescue => e
-
1
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
1
return
-
end
-
-
4
result = @impediment.errors.size
-
4
status = (result == 0 ? 200 : 400)
-
4
@include_meta = true
-
-
4
respond_to do |format|
-
8
format.html { render :partial => "impediment", :object => @impediment, :status => status }
-
end
-
end
-
-
1
def update
-
1
@impediment = RbTask.find_by_id(params[:id])
-
1
@settings = Backlogs.settings
-
1
begin
-
1
result = @impediment.update_with_relationships(params)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
1
status = (result ? 200 : 400)
-
1
@include_meta = true
-
-
1
respond_to do |format|
-
2
format.html { render :partial => "impediment", :object => @impediment, :status => status }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbMasterBacklogsController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
63
product_backlog_stories = RbStory.product_backlog(@project)
-
-
#collect all sprints which are sharing into @project
-
63
sprints = @project.open_shared_sprints
-
-
#TIB (ajout des sprints fermés)
-
63
c_sprints = @project.closed_shared_sprints
-
-
63
last_story = RbStory.find(
-
:first,
-
:conditions => ["project_id=? AND tracker_id in (?)", @project.id, RbStory.trackers],
-
:order => "updated_on DESC"
-
)
-
63
@last_update = (last_story ? last_story.updated_on : nil)
-
63
@product_backlog = { :sprint => nil, :stories => product_backlog_stories }
-
63
sprints_backlog_storie_of = RbStory.backlogs_by_sprint(@project, [sprints, c_sprints].flatten)
-
261
@sprint_backlogs = sprints.map{ |s| { :sprint => s, :stories => sprints_backlog_storie_of[s.id] } }
-
75
@c_sprint_backlogs = c_sprints.map{|s| { :sprint => s, :stories => sprints_backlog_storie_of[s.id] } }
-
-
63
respond_to do |format|
-
126
format.html { render :layout => "rb"}
-
end
-
end
-
-
1
def menu
-
39
links = []
-
-
39
if @settings[:sharing_enabled]
-
# FIXME: (pa sharing) usability is bad, menu is inconsistent. Sometimes we have a submenu with one entry, sometimes we have non-sharing behavior without submenu
-
39
unless @sprint #menu for product backlog
-
7
projects = @project.projects_in_shared_product_backlog
-
else #menu for sprint
-
32
projects = @sprint.shared_to_projects(@project)
-
end
-
#make the submenu or single link
-
39
if !projects.empty?
-
39
if projects.length > 1
-
32
links << {:label => l(:label_new_story), :url => '#', :sub => []}
-
32
projects.each{|project|
-
134
links.first[:sub] << {:label => project.name, :url => '#', :classname => "add_new_story project_id_#{project.id}"}
-
}
-
else
-
7
links << {:label => l(:label_new_story), :url => '#', :classname => "add_new_story project_id_#{projects[0].id}"}
-
end
-
end
-
else #no sharing, only own project in the menu
-
links << {:label => l(:label_new_story), :url => '#', :classname => 'add_new_story'}
-
end
-
-
links << {:label => l(:label_new_sprint), :url => '#', :classname => 'add_new_sprint'
-
39
} unless @sprint
-
links << {:label => l(:label_task_board),
-
:url => url_for(:controller => 'rb_taskboards', :action => 'show', :sprint_id => @sprint, :only_path => true)
-
39
} if @sprint && @sprint.stories.size > 0 && Backlogs.task_workflow(@project)
-
links << {:label => l(:label_burndown),
-
:url => '#',
-
:classname => 'show_burndown_chart'
-
39
} if @sprint && @sprint.stories.size > 0 && @sprint.has_burndown?
-
links << {:label => l(:label_stories_tasks),
-
:url => url_for(:controller => 'rb_queries', :action => 'show', :project_id => @project.id, :sprint_id => @sprint, :only_path => true)
-
39
} if @sprint && @sprint.stories.size > 0
-
links << {:label => l(:label_stories),
-
:url => url_for(:controller => 'rb_queries', :action => 'show', :project_id => @project, :only_path => true)
-
39
} unless @sprint
-
links << {:label => l(:label_sprint_cards),
-
:url => url_for(:controller => 'rb_stories', :action => 'index', :project_id => @project.identifier, :sprint_id => @sprint, :format => 'pdf', :only_path => true)
-
39
} if @sprint && BacklogsPrintableCards::CardPageLayout.selected && @sprint.stories.size > 0
-
links << {:label => l(:label_product_cards),
-
:url => url_for(:controller => 'rb_stories', :action => 'index', :project_id => @project.identifier, :format => 'pdf', :only_path => true)
-
39
} unless @sprint
-
links << {:label => l(:label_wiki),
-
:url => url_for(:controller => 'rb_wikis', :action => 'edit', :project_id => @project.id, :sprint_id => @sprint, :only_path => true)
-
231
} if @sprint && @project.enabled_modules.any? {|m| m.name=="wiki" }
-
links << {:label => l(:label_download_sprint),
-
:url => url_for(:controller => 'rb_sprints', :action => 'download', :sprint_id => @sprint, :format => 'xml', :only_path => true)
-
39
} if @sprint && @sprint.has_burndown?
-
links << {:label => l(:label_reset),
-
:url => url_for(:controller => 'rb_sprints', :action => 'reset', :sprint_id => @sprint, :only_path => true),
-
:warning => view_context().escape_javascript(l(:warning_reset_sprint)).gsub(/\/n/, "\n")
-
39
} if @sprint && @sprint.sprint_start_date && User.current.allowed_to?(:reset_sprint, @project)
-
-
-
39
respond_to do |format|
-
78
format.json { render :json => links }
-
end
-
end
-
-
1
if Rails::VERSION::MAJOR < 3
-
def view_context
-
@template
-
end
-
end
-
end
-
1
class RbQueriesController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
8
@query = Query.new(:name => "_")
-
8
@query.project = @project
-
-
8
if params[:sprint_id]
-
5
@query.add_filter("status_id", '*', ['']) # All statuses
-
5
@query.add_filter("fixed_version_id", '=', [params[:sprint_id]])
-
5
@query.add_filter("backlogs_issue_type", '=', ['any'])
-
else
-
3
@query.add_filter("status_id", 'o', ['']) # only open
-
3
@query.add_filter("fixed_version_id", '!*', ['']) # only unassigned
-
3
@query.add_filter("backlogs_issue_type", '=', ['story'])
-
end
-
-
56
column_names = @query.columns.collect{|col| col.name}
-
8
column_names = column_names + ['position'] unless column_names.include?('position')
-
-
8
session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :column_names => column_names}
-
8
redirect_to :controller => 'issues', :action => 'index', :project_id => @project.id, :sort => 'position'
-
end
-
-
1
def impediments
-
1
@query = Query.new(:name => "_")
-
1
@query.project = @project
-
1
@query.add_filter("status_id", 'o', ['']) # only open
-
1
@query.add_filter("fixed_version_id", '=', [params[:sprint_id]])
-
1
@query.add_filter("backlogs_issue_type", '=', ['impediment'])
-
1
session[:query] = {:project_id => @query.project_id, :filters => @query.filters }
-
1
redirect_to :controller => 'issues', :action => 'index', :project_id => @project.id
-
end
-
end
-
1
include RbCommonHelper
-
1
include ProjectsHelper
-
-
# Responsible for exposing release CRUD.
-
1
class RbReleasesController < RbApplicationController
-
1
unloadable
-
-
1
def index
-
6
@releases = RbRelease.find(:all, :conditions => { :project_id => @project })
-
end
-
-
1
def show
-
4
@remaining_story_points = remaining_story_points
-
-
4
respond_to do |format|
-
8
format.html { render }
-
4
format.csv { send_data(release_burndown_to_csv(@release), :type => 'text/csv; header=present', :filename => 'export.csv') }
-
end
-
end
-
-
1
def new
-
2
@release = RbRelease.new(:project => @project)
-
2
@backlog_points = remaining_story_points
-
2
@release.initial_story_points = @backlog_points
-
2
if request.post?
-
1
@release.attributes = params[:release]
-
1
if @release.save
-
1
flash[:notice] = l(:notice_successful_create)
-
1
redirect_to :action => 'index', :project_id => @project
-
end
-
end
-
end
-
-
1
def edit
-
2
if request.post? and @release.update_attributes(params[:release])
-
1
flash[:notice] = l(:notice_successful_update)
-
1
redirect_to :controller => 'rb_releases', :action => 'show', :release_id => @release
-
else
-
1
@backlog_points = remaining_story_points
-
end
-
end
-
-
1
def destroy
-
1
@release.destroy
-
1
redirect_to :controller => 'rb_releases', :action => 'index', :project_id => @project
-
end
-
-
1
def snapshot
-
rbdd = @release.today
-
unless rbdd
-
rbdd = ReleaseBurndownDay.new
-
rbdd.release_id = @release.id
-
rbdd.day = Date.today
-
end
-
rbdd.remaining_story_points = remaining_story_points
-
rbdd.save!
-
redirect_to :controller => 'rb_releases', :action => 'show', :release_id => @release
-
end
-
-
1
private
-
-
1
def remaining_story_points
-
7
res = 0
-
49
@release.stories.each {|s| res += s.story_points if s.story_points}
-
7
res
-
end
-
-
end
-
1
class RbServerVariablesController < RbApplicationController
-
1
unloadable
-
-
# for index there's no @project
-
# (eliminates the need of RbAllProjectsController)
-
1
skip_before_filter :load_project, :authorize, :only => [:index]
-
-
1
def index
-
115
@context = params[:context]
-
115
respond_to do |format|
-
115
format.html { render_404 }
-
230
format.js { render :file => 'rb_server_variables/show.js.erb', :layout => false }
-
end
-
end
-
-
1
alias :project :index
-
1
alias :sprint :index
-
end
-
1
include RbCommonHelper
-
-
# Responsible for exposing sprint CRUD. It SHOULD NOT be used
-
# for displaying the taskboard since the taskboard is a management
-
# interface used for managing objects within a sprint. For
-
# info about the taskboard, see RbTaskboardsController
-
1
class RbSprintsController < RbApplicationController
-
1
unloadable
-
-
1
def create
-
14
attribs = params.select{|k,v| k != 'id' and RbSprint.column_names.include? k }
-
1
attribs = Hash[*attribs.flatten]
-
1
@sprint = RbSprint.new(attribs)
-
-
1
begin
-
1
@sprint.save!
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
1
result = @sprint.errors.size
-
1
status = (result == 0 ? 200 : 400)
-
-
1
respond_to do |format|
-
2
format.html { render :partial => "sprint", :status => status, :locals => { :sprint => @sprint } }
-
end
-
end
-
-
1
def update
-
32
attribs = params.select{|k,v| k != 'id' and RbSprint.column_names.include? k }
-
2
attribs = Hash[*attribs.flatten]
-
2
begin
-
2
result = @sprint.update_attributes attribs
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
2
respond_to do |format|
-
4
format.html { render :partial => "sprint", :status => (result ? 200 : 400), :locals => { :sprint => @sprint } }
-
end
-
end
-
-
1
def download
-
bold = {:font => {:bold => true}}
-
dump = BacklogsSpreadsheet::WorkBook.new
-
ws = dump[@sprint.name]
-
ws << [nil, @sprint.id, nil, nil, {:value => @sprint.name, :style => bold}, {:value => 'Start', :style => bold}] + @sprint.days(:all).collect{|d| {:value => d, :style => bold} }
-
bd = @sprint.burndown
-
bd.series(false).sort{|a, b| l("label_#{a}") <=> l("label_#{b}")}.each{ |k|
-
ws << [ nil, nil, nil, nil, l("label_#{k}") ] + bd[k]
-
}
-
-
@sprint.stories.each{|s|
-
ws << [s.tracker.name, s.id, nil, nil, {:value => s.subject, :style => bold}]
-
bd = s.burndown
-
bd.keys.sort{|a, b| l("label_#{a}") <=> l("label_#{b}")}.each{ |k|
-
next if k == :status
-
label = l("label_#{k}")
-
label = {:value => label, :comment => k.to_s} if [:points, :points_accepted].include?(k)
-
ws << [nil, nil, nil, nil, label ] + bd[k]
-
}
-
s.tasks.each {|t|
-
ws << [nil, nil, t.tracker.name, t.id, {:value => t.subject, :style => bold}] + t.burndown
-
}
-
}
-
-
send_data(dump.to_xml, :disposition => 'attachment', :type => 'application/vnd.ms-excel', :filename => "#{@project.identifier}-#{@sprint.name.gsub(/[^a-z0-9]/i, '')}.xml")
-
end
-
-
1
def reset
-
unless @sprint.sprint_start_date
-
render :text => 'Sprint without start date cannot be reset', :status => 400
-
return
-
end
-
-
ids = []
-
status = IssueStatus.default.id
-
Issue.find(:all, :conditions => ['fixed_version_id = ?', @sprint.id]).each {|issue|
-
ids << issue.id.to_s
-
issue.update_attributes!(:created_on => @sprint.sprint_start_date.to_time, :status_id => status)
-
}
-
if ids.size != 0
-
ids = ids.join(',')
-
Issue.connection.execute("update issues set updated_on = created_on where id in (#{ids})")
-
-
Journal.connection.execute("delete from journal_details where journal_id in (select id from journals where journalized_type = 'Issue' and journalized_id in (#{ids}))")
-
Journal.connection.execute("delete from journals where (notes is null or notes = '') and journalized_type = 'Issue' and journalized_id in (#{ids})")
-
Journal.connection.execute("update journals
-
set created_on = (select created_on
-
from issues
-
where journalized_id = issues.id)
-
where journalized_type = 'Issue' and journalized_id in (#{ids})")
-
end
-
-
redirect_to :controller => 'rb_master_backlogs', :action => 'show', :project_id => @project.identifier
-
end
-
-
1
def close_completed
-
@project.close_completed_versions if request.put?
-
-
redirect_to :controller => 'rb_master_backlogs', :action => 'show', :project_id => @project
-
end
-
end
-
1
require 'prawn'
-
1
require 'backlogs_printable_cards'
-
-
1
include RbCommonHelper
-
-
1
class RbStoriesController < RbApplicationController
-
1
unloadable
-
1
include BacklogsPrintableCards
-
-
1
def index
-
2
if ! BacklogsPrintableCards::CardPageLayout.selected
-
render :text => "No label stock selected. How did you get here?", :status => 500
-
return
-
end
-
-
2
begin
-
2
cards = BacklogsPrintableCards::PrintableCards.new(params[:sprint_id] ? @sprint.stories : RbStory.product_backlog(@project), params[:sprint_id], current_language)
-
rescue Prawn::Errors::CannotFit
-
render :text => "There was a problem rendering the cards. A possible error could be that the selected font exceeds a render box", :status => 500
-
return
-
end
-
-
2
respond_to do |format|
-
2
format.pdf {
-
2
send_data(cards.pdf.render, :disposition => 'attachment', :type => 'application/pdf')
-
}
-
end
-
end
-
-
1
def create
-
1
params['author_id'] = User.current.id
-
1
begin
-
1
story = RbStory.create_and_position(params)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
1
status = (story.id ? 200 : 400)
-
-
1
respond_to do |format|
-
2
format.html { render :partial => "story", :object => story, :status => status }
-
end
-
end
-
-
1
def update
-
33
story = RbStory.find(params[:id])
-
33
begin
-
33
result = story.update_and_position!(params)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
33
status = (result ? 200 : 400)
-
-
33
respond_to do |format|
-
66
format.html { render :partial => "story", :object => story, :status => status }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbTaskboardsController < RbApplicationController
-
1
unloadable
-
-
1
def show
-
44
stories = @sprint.stories
-
167
@story_ids = stories.map{|s| s.id}
-
-
44
@settings = Backlogs.settings
-
-
## determine status columns to show
-
44
tracker = Tracker.find_by_id(RbTask.tracker)
-
44
statuses = tracker.issue_statuses
-
# disable columns by default
-
44
if User.current.admin?
-
@statuses = statuses
-
else
-
44
enabled = {}
-
308
statuses.each{|s| enabled[s.id] = false}
-
# enable all statuses held by current tasks, regardless of whether the current user has access
-
301
RbTask.find(:all, :conditions => ['fixed_version_id = ?', @sprint.id]).each {|task| enabled[task.status_id] = true }
-
-
44
roles = User.current.roles_for_project(@project)
-
#@transitions = {}
-
44
statuses.each {|status|
-
-
# enable all statuses the current user can reach from any task status
-
264
[false, true].each {|creator|
-
528
[false, true].each {|assignee|
-
-
6336
allowed = status.new_statuses_allowed_to(roles, tracker, creator, assignee).collect{|s| s.id}
-
#@transitions["c#{creator ? 'y' : 'n'}a#{assignee ? 'y' : 'n'}"] = allowed
-
6336
allowed.each{|s| enabled[s] = true}
-
}
-
}
-
}
-
308
@statuses = statuses.select{|s| enabled[s.id]}
-
end
-
-
44
if @sprint.stories.size == 0
-
1
@last_updated = nil
-
else
-
43
@last_updated = RbTask.find(:first,
-
:conditions => ['tracker_id = ? and fixed_version_id = ?', RbTask.tracker, @sprint.stories[0].fixed_version_id],
-
:order => "updated_on DESC")
-
end
-
-
44
respond_to do |format|
-
88
format.html { render :layout => "rb" }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbTasksController < RbApplicationController
-
1
unloadable
-
-
1
def create
-
10
@settings = Backlogs.settings
-
10
@task = nil
-
10
begin
-
10
@task = RbTask.create_with_relationships(params, User.current.id, @project.id)
-
rescue => e
-
render :text => e.message.blank? ? e.to_s : e.message, :status => 400
-
return
-
end
-
-
10
result = @task.errors.size
-
10
status = (result == 0 ? 200 : 400)
-
10
@include_meta = true
-
-
10
respond_to do |format|
-
20
format.html { render :partial => "task", :object => @task, :status => status }
-
end
-
end
-
-
1
def update
-
4
@task = RbTask.find_by_id(params[:id])
-
4
@settings = Backlogs.settings
-
4
result = @task.update_with_relationships(params)
-
4
status = (result ? 200 : 400)
-
4
@include_meta = true
-
-
4
respond_to do |format|
-
8
format.html { render :partial => "task", :object => @task, :status => status }
-
end
-
end
-
-
end
-
1
include RbCommonHelper
-
-
1
class RbUpdatedItemsController < RbApplicationController
-
1
unloadable
-
-
# Returns all models that have changed since params[:since]
-
# params[:only] limits the types of models that the method
-
# should return
-
1
def show
-
8
@settings = Backlogs.settings
-
16
only = (params[:only] ? params[:only].split(/, ?/).map{|v| v.to_sym} : [:sprints, :stories, :tasks, :impediments])
-
8
@items = HashWithIndifferentAccess.new
-
8
@include_meta = true
-
8
@last_update = nil
-
-
8
latest_updates = []
-
8
if only.include? :stories
-
2
@items[:stories] = RbStory.find_all_updated_since(params[:since], @project.id)
-
2
if @items[:stories].length > 0
-
8
latest_updates << @items[:stories].sort{ |a,b| a.updated_on <=> b.updated_on }.last
-
end
-
end
-
-
8
if only.include? :tasks
-
2
@items[:tasks] = RbTask.find_all_updated_since(params[:since], @project.id)
-
2
if @items[:tasks].length > 0
-
2
latest_updates << @items[:tasks].sort{ |a,b| a.updated_on <=> b.updated_on }.last
-
end
-
end
-
-
8
if only.include? :impediments
-
4
@items[:impediments] = RbTask.find_all_updated_since(params[:since], @project.id, true)
-
4
if @items[:impediments].length > 0
-
4
latest_updates << @items[:impediments].sort{ |a,b| a.updated_on <=> b.updated_on }.last
-
end
-
end
-
-
8
if latest_updates.length > 0
-
6
@last_update = latest_updates.sort{ |a,b| a.updated_on <=> b.updated_on }.last.updated_on
-
end
-
-
8
respond_to do |format|
-
16
format.html { render :layout => false }
-
end
-
end
-
end
-
1
class RbWikisController < RbApplicationController
-
1
unloadable
-
-
# NOTE: This method is public (see init.rb). We will let Redmine core's
-
# WikiController#index tak care of autorization
-
# NOTE: this redirection causes a page to be created from a template
-
# as a side-effect of calling @sprint.wiki_page
-
1
def show
-
2
redirect_to :controller => 'wiki', :action => 'index', :project_id => @project.id, :id => @sprint.wiki_page
-
end
-
-
# NOTE: This method is public (see init.rb). We will let Redmine core's
-
# WikiController#index tak care of autorization
-
# NOTE: this redirection causes a page to be created from a template
-
# as a side-effect of calling @sprint.wiki_page
-
1
def edit
-
redirect_to :controller => 'wiki', :action => 'edit', :project_id => @project.id, :id => @sprint.wiki_page
-
end
-
end
-
1
require 'color'
-
1
require 'nokogiri'
-
-
1
module RbCommonHelper
-
1
unloadable
-
-
1
def assignee_id_or_empty(story)
-
464
story.new_record? ? "" : story.assigned_to_id
-
end
-
-
1
def assignee_name_or_empty(story)
-
370
story.blank? || story.assigned_to.blank? ? "" : "#{story.assigned_to.name}"
-
end
-
-
1
def blocked_ids(blocked)
-
175
blocked.map{|b| b.id }.join(',')
-
end
-
-
1
def build_inline_style(task)
-
370
if (task.blank? || task.assigned_to.blank? || !task.assigned_to.is_a?(User))
-
370
''
-
else
-
color_to = task.assigned_to.backlogs_preference[:task_color]
-
color_from = Backlogs::Color.new(color_to).lighten(0.5)
-
"style='
-
background-color:#{task.assigned_to.backlogs_preference[:task_color]};
-
background: -webkit-gradient(linear, left top, left bottom, from(#{color_from}), to(#{color_to}));
-
background: -moz-linear-gradient(top, #{color_from}, #{color_to});
-
filter:progid:DXImageTransform.Microsoft.Gradient(Enabled=1,GradientType=0,StartColorStr=#{color_from},EndColorStr=#{color_to});
-
'"
-
end
-
end
-
-
1
def breadcrumb_separator
-
151
"<span class='separator'>></span>".html_safe
-
end
-
-
1
def description_or_empty(story)
-
story.new_record? ? "" : textilizable(story, :description)
-
end
-
-
1
def id_or_empty(item)
-
1013
item.new_record? ? "" : item.id
-
end
-
-
1
def issue_link_or_empty(item)
-
737
item_id = item.id.to_s
-
737
text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id)
-
737
item.new_record? ? "" : link_to(text, {:controller => "issues", :action => "show", :id => item}, {:target => "_blank", :class => "prevent_edit"})
-
end
-
-
1
def sprint_link_or_empty(item)
-
276
item_id = item.id.to_s
-
276
text = (item_id.length > 8 ? "#{item_id[0..1]}...#{item_id[-4..-1]}" : item_id)
-
276
item.new_record? ? "" : link_to(text, {:controller => 'versions', :action => "show", :id => item}, {:target => "_blank", :class => "prevent_edit"})
-
end
-
-
1
def release_link_or_empty(release)
-
12
release.new_record? ? "" : link_to(release.name, {:controller => "rb_releases", :action => "show", :release_id => release})
-
end
-
-
1
def mark_if_closed(story)
-
860
!story.new_record? && story.status.is_closed? ? "closed" : ""
-
end
-
-
1
def story_points_or_empty(story)
-
1731
story.story_points.blank? ? "" : story.story_points
-
end
-
-
1
def record_id_or_empty(story)
-
story.new_record? ? "" : story.id
-
end
-
-
1
def sprint_status_id_or_default(sprint)
-
sprint.new_record? ? Version::VERSION_STATUSES.first : sprint.status
-
end
-
-
1
def sprint_status_label_or_default(sprint)
-
sprint.new_record? ? l("version_status_#{Version::VERSION_STATUSES.first}") : l("version_status_#{sprint.status}")
-
end
-
-
1
def status_id_or_default(story)
-
490
story.new_record? ? IssueStatus.default.id : story.status.id
-
end
-
-
1
def status_label_or_default(story)
-
1241
story.new_record? ? IssueStatus.default.name : story.status.name
-
end
-
-
1
def sprint_html_id_or_empty(sprint)
-
276
sprint.new_record? ? "" : "sprint_#{sprint.id}"
-
end
-
-
1
def story_html_id_or_empty(story)
-
490
story.new_record? ? "" : "story_#{story.id}"
-
end
-
-
1
def release_html_id_or_empty(release)
-
12
release.new_record? ? "" : "release_#{release.id}"
-
end
-
-
1
def textile_description_or_empty(story)
-
story.new_record? ? "" : h(story.description).gsub(/<(\/?pre)>/, '<\1>')
-
end
-
-
1
def textile_to_html(textile)
-
751
textile.nil? ? "" : Redmine::WikiFormatting::Textile::Formatter.new(textile).to_html
-
end
-
-
1
def tracker_id_or_empty(story)
-
490
story.new_record? ? "" : story.tracker_id
-
end
-
-
1
def tracker_name_or_empty(story)
-
490
story.new_record? ? "" : story.tracker.name
-
end
-
-
1
def updated_on_with_milliseconds(story)
-
date_string_with_milliseconds(story.updated_on, 0.001) unless story.blank?
-
end
-
-
1
def date_string_with_milliseconds(d, add=0)
-
115
return '' if d.blank?
-
109
d.strftime("%B %d, %Y %H:%M:%S") + '.' + (d.to_f % 1 + add).to_s.split('.')[1] + d.strftime(" %z")
-
end
-
-
1
def remaining_hours(item)
-
109
item.remaining_hours.blank? || item.remaining_hours==0 ? "" : item.remaining_hours
-
end
-
-
1
def workdays(start_day, end_day)
-
240
return (start_day .. end_day).select {|d| (d.wday > 0 and d.wday < 6) }
-
end
-
-
1
def release_burndown_interpolate(release, day)
-
initial_day = release.burndown.days[0]
-
initial_points = release.burndown.remaining_story_points[0]
-
day_diff = initial_points / (release.days.size - 1.0)
-
initial_points - ( (workdays(initial_day, day).size - 1) * day_diff )
-
end
-
-
1
def release_burndown_to_csv(release)
-
ic = Iconv.new(l(:general_csv_encoding), 'UTF-8')
-
-
# FIXME decimal_separator is not used, instead a hardcoded s/\./,/g is done
-
# below to make (German) Excel happy
-
#decimal_separator = l(:general_csv_decimal_separator)
-
-
export = FCSV.generate(:col_sep => ';') do |csv|
-
# csv header fields
-
headers = [ l(:label_date),
-
l(:remaining_story_points),
-
l(:ideal)
-
]
-
csv << headers.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
-
-
# csv lines
-
if (release.release_start_date != release.burndown_days[0])
-
fields = [release.release_start_date,
-
release.initial_story_points.to_f.to_s.gsub('.', ','),
-
release.initial_story_points.to_f.to_s.gsub('.', ',')]
-
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
-
end
-
release.burndown_days.each do |rbd|
-
fields = [rbd.day,
-
rbd.remaining_story_points.to_s.gsub('.', ','),
-
release_burndown_interpolate(release, rbd.day).to_s.gsub('.', ',')
-
]
-
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
-
end
-
if (release.release_end_date != release.burndown_days[-1])
-
fields = [release.release_end_date, "", "0,0"]
-
csv << fields.collect {|c| begin; ic.iconv(c.to_s); rescue; c.to_s; end }
-
end
-
end
-
export
-
end
-
-
# Renders the project quick-jump box
-
1
def render_backlog_project_jump_box
-
107
projects = EnabledModule.find(:all,
-
:conditions => ["enabled_modules.name = 'backlogs' and status = ?", Project::STATUS_ACTIVE],
-
:include => :project,
-
219
:joins => :project).collect { |mod| mod.project}
-
-
326
projects = Member.find(:all, :conditions => ["user_id = ? and project_id IN (?)", User.current.id, projects.collect(&:id)]).collect{ |m| m.project}
-
-
107
if projects.any?
-
107
s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
-
"<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
-
'<option value="" disabled="disabled">---</option>'
-
s << project_tree_options_for_select(projects, :selected => @project) do |p|
-
219
{ :value => url_for(:controller => 'rb_master_backlogs', :action => 'show', :project_id => p, :jump => current_menu_item) }
-
107
end
-
107
s << '</select>'
-
107
s.html_safe
-
end
-
end
-
-
# Returns a collection of users allowed to log time for the current project. (see app/views/rb_taskboards/show.html.erb for usage)
-
1
def users_allowed_to_log_on_task
-
44
@project.memberships.collect{|m|
-
132
user = m.user
-
132
roles = user ? user.roles_for_project(@project) : nil
-
264
roles && roles.detect {|role| role.member? && role.allowed_to?(:log_time)} ? [user.name, user.id] : nil
-
}.compact.insert(0,["",0]) # Add blank entry
-
end
-
-
1
def tidy(html)
-
751
return Nokogiri::HTML::fragment(html).to_xhtml
-
end
-
-
1
def users_assignable_options_for_select(collection)
-
44
s = ''
-
44
groups = ''
-
44
collection.sort.each do |element|
-
88
if element.is_a?(Group)
-
groups << "<option value=\"#{element.id}\" color=\"#AAAAAA\" color_light=\"#E0E0E0\">#{h element.name}</option>"
-
else
-
88
s << "<option value=\"#{element.id}\" color=\"#{element.backlogs_preference[:task_color]}\" color_light=\"#{element.backlogs_preference[:task_color_light]}\">#{h element.name}</option>"
-
end
-
end
-
44
unless groups.empty?
-
s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
-
end
-
44
s.html_safe
-
end
-
-
# Streamline the difference between <%= %> and <% %>
-
1
def rb_labelled_fields_for(*args, &proc)
-
2
fields_string = labelled_fields_for(*args, &proc)
-
2
if Rails::VERSION::MAJOR < 3
-
fields_string
-
else
-
2
concat(fields_string)
-
end
-
end
-
-
# Streamline the difference between <%= %> and <% %>
-
1
def rb_labelled_form_for(*args, &proc)
-
2
form_string = labelled_form_for(*args, &proc)
-
2
if Rails::VERSION::MAJOR < 3
-
form_string
-
else
-
2
concat(form_string)
-
end
-
end
-
-
end
-
1
module RbMasterBacklogsHelper
-
1
unloadable
-
1
include Redmine::I18n
-
-
1
def backlog_html_class(backlog)
-
is_sprint?(backlog) ? "sprint backlog" : "product backlog"
-
end
-
-
1
def backlog_html_id(backlog)
-
is_sprint?(backlog) ? "sprint_#{backlog.id}" : "product_backlog"
-
end
-
-
1
def backlog_id_or_empty(backlog)
-
is_sprint?(backlog) ? backlog.id : ""
-
end
-
-
1
def backlog_menu(is_sprint, items = [])
-
html = %{
-
<div class="menu">
-
<div class="icon ui-icon ui-icon-carat-1-s"></div>
-
<ul class="items">
-
}
-
items.each do |item|
-
item[:condition] = true unless item.has_key?(:condition)
-
if item[:condition] && ( (is_sprint && item[:for] == :sprint) ||
-
(!is_sprint && item[:for] == :product) ||
-
(item[:for] == :both) )
-
html += %{ <li class="item">#{item[:item]}</li> }
-
end
-
end
-
html += %{
-
</ul>
-
</div>
-
}
-
end
-
-
1
def date_or_nil(date)
-
date.blank? ? '' : date.strftime('%Y-%m-%d')
-
end
-
-
1
def editable_if_sprint(backlog)
-
"editable" if is_sprint?(backlog)
-
end
-
-
1
def is_sprint?(backlog)
-
backlog.class.to_s.downcase=='sprint'
-
end
-
-
1
def menu_link(label, options = {})
-
# options[:class] = "pureCssMenui"
-
link_to(label, options)
-
end
-
-
1
def name_or_default(backlog)
-
is_sprint?(backlog) ? backlog.name : l(:label_Product_backlog)
-
end
-
-
1
def stories(backlog)
-
backlog[:stories] || backlog.stories
-
end
-
end
-
# Redmine - project management software
-
# Copyright (C) 2006-2011 Jean-Philippe Lang
-
#
-
# This program is free software; you can redistribute it and/or
-
# modify it under the terms of the GNU General Public License
-
# as published by the Free Software Foundation; either version 2
-
# of the License, or (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program; if not, write to the Free Software
-
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
-
-
1
class RbJournal < ActiveRecord::Base
-
1
REDMINE_PROPERTIES = ['estimated_hours', 'fixed_version_id', 'status_id', 'story_points', 'remaining_hours']
-
1
JOURNALED_PROPERTIES = {
-
'estimated_hours' => :float,
-
'remaining_hours' => :float,
-
'story_points' => :float,
-
'fixed_version_id' => :int,
-
'status_open' => :bool,
-
'status_success' => :bool,
-
}
-
-
1
belongs_to :issue
-
-
1
def self.journal(j)
-
119
j.rb_journal_properties_saved ||= []
-
-
119
case Backlogs.platform
-
when :redmine
-
119
j.details.each{|detail|
-
164
next if j.rb_journal_properties_saved.include?(detail.prop_key)
-
164
next unless detail.property == 'attr' && RbJournal::REDMINE_PROPERTIES.include?(detail.prop_key)
-
145
j.rb_journal_properties_saved << detail.prop_key
-
145
create_journal(j, detail, j.journalized_id, j.created_on)
-
}
-
-
when :chiliproject
-
if j.type == 'IssueJournal'
-
RbJournal::REDMINE_PROPERTIES.each{|prop|
-
next if j.details[prop].nil?
-
create_journal(j, prop, j.journaled_id, j.created_on)
-
}
-
end
-
end
-
end
-
-
1
def self.create_journal(j, prop, issue_id, timestamp)
-
145
if journal_property_key(prop) == 'status_id'
-
55
begin
-
55
status = IssueStatus.find(journal_property_value(prop, j))
-
rescue ActiveRecord::RecordNotFound
-
status = nil
-
end
-
55
changes = [ { :property => 'status_open', :value => status && !status.is_closed },
-
{ :property => 'status_success', :value => status && !status.backlog_is?(:success) } ]
-
else
-
90
changes = [ { :property => journal_property_key(prop), :value => journal_property_value(prop, j) } ]
-
end
-
145
changes.each{|change|
-
200
RbJournal.new(:issue_id => issue_id, :timestamp => timestamp, :change => change).save
-
}
-
end
-
-
1
def self.journal_property_key(property)
-
235
case Backlogs.platform
-
when :redmine
-
235
return property.prop_key
-
when :chiliproject
-
return property
-
end
-
end
-
-
1
def self.journal_property_value(property, j)
-
145
case Backlogs.platform
-
when :redmine
-
145
return property.value
-
when :chiliproject
-
return j.details[property][1]
-
end
-
end
-
-
1
def self.rebuild(issue)
-
996
RbJournal.delete_all(:issue_id => issue.id)
-
-
996
changes = {}
-
5976
RbJournal::REDMINE_PROPERTIES.each{|prop| changes[prop] = [] }
-
-
996
case Backlogs.platform
-
when :redmine
-
996
JournalDetail.find(:all, :order => "journals.created_on asc" , :joins => :journal,
-
:conditions => ["property = 'attr' and prop_key in (?)
-
and journalized_type = 'Issue' and journalized_id = ?",
-
RbJournal::REDMINE_PROPERTIES, issue.id]).each {|detail|
-
changes[detail.prop_key] << {:time => detail.journal.created_on, :old => detail.old_value, :new => detail.value}
-
}
-
-
when :chiliproject
-
# has to be here because the ChiliProject journal design easily allows for one to delete issue statuses that remain
-
# in the journal, because even the already-feeble rails integrity constraints can't be enforced. This also mean it's
-
# not really reliable to use statuses for historic burndown calculation. Those are the breaks if you let programmers
-
# do database design.
-
valid_statuses = IssueStatus.connection.select_values("select id from #{IssueStatus.table_name}").collect{|x| x.to_i}
-
-
issue.journals.reject{|j| j.created_at < issue.created_on}.each{|j|
-
RbJournal::REDMINE_PROPERTIES.each{|prop|
-
delta = j.changes[prop]
-
next unless delta
-
if prop == 'status_id'
-
next if changes[prop].size == 0 && !valid_statuses.include?(delta[0])
-
next unless valid_statuses.include?(delta[1])
-
end
-
changes[prop] << {:time => j.created_at, :old => delta[0], :new => delta[1]}
-
}
-
}
-
end
-
-
996
RbJournal::REDMINE_PROPERTIES.each{|prop|
-
4980
if changes[prop].size > 0
-
changes[prop].unshift({:time => issue.created_on, :new => changes[prop][0][:old]})
-
else
-
4980
changes[prop] = [{:time => issue.created_on, :new => issue.send(prop.intern)}]
-
end
-
}
-
-
996
issue_status = {}
-
1992
changes['status_id'].collect{|change| change[:new]}.compact.uniq.each{|id|
-
996
begin
-
996
issue_status[id] = IssueStatus.find(Integer(id))
-
rescue ActiveRecord::RecordNotFound
-
issue_status[id] = nil
-
end
-
}
-
-
2988
['status_open', 'status_success'].each{|p| changes[p] = [] }
-
996
changes['status_id'].each{|change|
-
996
status = issue_status[change[:new]]
-
996
changes['status_open'] << change.merge(:new => status && !status.is_closed?)
-
996
changes['status_success'] << change.merge(:new => status && status.backlog_is?(:success))
-
}
-
996
changes.delete('status_id')
-
-
996
changes.each_pair{|prop, updates|
-
5976
updates.each{|change|
-
5976
RbJournal.new(:issue_id => issue.id, :timestamp => change[:time], :change => {:property => prop, :value => change[:new]}).save
-
}
-
}
-
end
-
-
1
def to_s
-
"<#{RbJournal.table_name} issue=#{issue_id}: #{property}=#{value.inspect} @ #{timestamp}>"
-
end
-
-
1
def self.changes_to_s(changes, prefix = '')
-
s = ''
-
changes.each_pair{|k, v|
-
s << "#{prefix}#{k}\n"
-
v.each{|change|
-
s << "#{prefix} @#{change[:time]}: #{change[:new]}\n"
-
}
-
}
-
return s
-
end
-
-
1
def change=(prop)
-
6176
self.property = prop[:property]
-
6176
self.value = prop[:value]
-
end
-
1
def property
-
6176
return self[:property].to_sym
-
end
-
1
def property=(name)
-
6176
name = name.to_s
-
6176
raise "Unknown journal property #{name.inspect}" unless RbJournal::JOURNALED_PROPERTIES.include?(name)
-
6176
self[:property] = name
-
end
-
-
1
def value
-
3257
v = self[:value]
-
-
# this test against blank *only* works when not storing string properties! Otherwise test against nil? here and handle
-
# blank? per-type
-
3257
return nil if v.blank?
-
-
3179
case RbJournal::JOURNALED_PROPERTIES[self[:property]]
-
when :bool
-
1120
return (v == 'true')
-
when :int
-
760
return Integer(v)
-
when :float
-
1299
return Float(v)
-
else
-
raise "Unknown journal property #{self[:property].inspect}"
-
end
-
end
-
1
def value=(v)
-
# this test against blank *only* works when not storing string properties! Otherwise test against nil? here and handle
-
# blank? per-type
-
6176
self[:value] = v.blank? ? nil : case RbJournal::JOURNALED_PROPERTIES[self[:property]]
-
when :bool
-
1052
v ? 'true' : 'false'
-
when :int, :float
-
1664
v.to_s
-
else
-
raise "Unknown journal property #{self[:property].inspect}"
-
end
-
end
-
end
-
1
require 'date'
-
-
# FIXME this is simplified copypasta of the Burndown class from sprint.rb
-
1
class ReleaseBurndown
-
1
class Series < Array
-
1
def initialize(*args)
-
8
@name = args.pop
-
-
8
raise "Name '#{@name}' must be a symbol" unless @name.is_a? Symbol
-
8
super(*args)
-
end
-
-
1
attr_reader :name
-
end
-
-
1
def initialize(release)
-
4
@days = release.days
-
4
@release_id = release.id
-
-
# end date for graph
-
4
days = @days
-
4
daycount = days.size
-
#days = release.days(Date.today) if release.release_end_date > Date.today
-
-
4
_series = ([nil] * days.size)
-
-
# load cache
-
4
day_index = to_h(days, (0..(days.size - 1)).to_a)
-
4
ReleaseBurndownDay.find(:all, :order=>'day', :conditions => ["release_id = ?", release.id]).each {|data|
-
day = day_index[data.day.to_date]
-
next if !day
-
-
_series[day] = [data.remaining_story_points.to_f]
-
}
-
-
# use initial story points for first day if not loaded from cache (db)
-
4
_series[0] = [release.initial_story_points.to_f] unless _series[0]
-
-
# fill out series
-
4
last = nil
-
168
_series = _series.enum_for(:each_with_index).collect{|v, i| v.nil? ? last : (last = v; v) }
-
-
# make registered series
-
4
remaining_story_points = _series.transpose
-
4
make_series :remaining_story_points, remaining_story_points[0]
-
-
# calculate burn-down ideal
-
4
if daycount == 1 # should never happen
-
make_series :ideal, [remaining_story_points[0]]
-
else
-
4
day_diff = remaining_story_points[0][0] / (daycount - 1.0)
-
168
make_series :ideal, remaining_story_points[0].enum_for(:each_with_index).collect{|c, i| remaining_story_points[0][0] - i * day_diff }
-
end
-
-
4
@max = @available_series.values.flatten.compact.max
-
end
-
-
1
attr_reader :days
-
1
attr_reader :release_id
-
1
attr_reader :max
-
-
1
attr_reader :remaining_story_points
-
1
attr_reader :ideal
-
-
1
def series(select = :active)
-
64
return @available_series.values.select{|s| (select == :all) }.sort{|x,y| "#{x.name}" <=> "#{y.name}"}
-
end
-
-
1
private
-
-
1
def make_series(name, data)
-
8
@available_series ||= {}
-
8
s = ReleaseBurndown::Series.new(data, name)
-
8
@available_series[name] = s
-
8
instance_variable_set("@#{name}", s)
-
end
-
-
1
def to_h(keys, values)
-
4
return Hash[*keys.zip(values).flatten]
-
end
-
-
end
-
-
1
class RbRelease < ActiveRecord::Base
-
1
self.table_name = 'releases'
-
-
1
unloadable
-
-
1
belongs_to :project
-
1
has_many :release_burndown_days, :dependent => :delete_all, :foreign_key => :release_id
-
-
1
validates_presence_of :project_id, :name, :release_start_date, :release_end_date, :initial_story_points
-
1
validates_length_of :name, :maximum => 64
-
1
validate :dates_valid?
-
-
1
include Backlogs::ActiveRecord::Attributes
-
-
1
def dates_valid?
-
10
errors.add_to_base(l(:error_release_end_after_start)) if self.release_start_date >= self.release_end_date if self.release_start_date and self.release_end_date
-
end
-
-
1
def stories
-
7
return RbStory.stories_open(project)
-
end
-
-
1
def burndown_days
-
28
self.release_burndown_days.sort { |a,b| a.day <=> b.day }
-
end
-
-
1
def days(cutoff = nil)
-
# assumes mon-fri are working days, sat-sun are not. this
-
# assumption is not globally right, we need to make this configurable.
-
4
cutoff = self.release_end_date if cutoff.nil?
-
4
workdays(self.release_start_date, cutoff)
-
end
-
-
1
def has_burndown?
-
20
return !!(self.release_start_date and self.release_end_date and self.initial_story_points)
-
end
-
-
1
def burndown
-
4
return nil if not self.has_burndown?
-
4
@cached_burndown ||= ReleaseBurndown.new(self)
-
4
return @cached_burndown
-
end
-
-
1
def today
-
4
ReleaseBurndownDay.find(:first, :conditions => { :release_id => self, :day => Date.today })
-
end
-
-
1
def js_ideal
-
4
"[['#{release_start_date}', #{initial_story_points}], ['#{release_end_date}', 0]]"
-
end
-
-
1
def js_snapshots
-
4
foo = "["
-
4
foo += "['#{release_start_date}', #{initial_story_points}]," if burndown_days and burndown_days[0] and burndown_days[0].day != release_start_date
-
4
burndown_days.each { |bdd| foo += "['#{bdd.day}', #{bdd.remaining_story_points}]," }
-
4
foo += "]"
-
end
-
end
-
1
require 'date'
-
-
1
class Burndown
-
1
def initialize(sprint, direction)
-
26
@direction = direction
-
26
@sprint_id = sprint.id
-
26
@days = sprint.days(:all)
-
-
26
baseline = [0] * (sprint.days(:active).size + 1)
-
26
baseline += [nil] * (1 + (@days.size - baseline.size))
-
-
26
series = Backlogs::MergedArray.new
-
26
series.merge(:hours => baseline.dup)
-
26
series.merge(:points => baseline.dup)
-
26
series.merge(:points_resolved => baseline.dup)
-
26
series.merge(:points_accepted => baseline.dup)
-
-
26
if RbStory.trackers.size > 0
-
26
stories = sprint.stories + RbStory.find(:all,
-
:joins => ["JOIN rb_journals ON rb_journals.issue_id = issues.id and property = 'fixed_version_id' and value = '#{sprint.id}'"],
-
:conditions => ["tracker_id in (?) and fixed_version_id <> #{sprint.id}", RbStory.trackers])
-
-
85
stories.each { |story| series.add(story.burndown(sprint)) }
-
-
517
series.merge(:to_resolve => series.collect{|r| r.points && r.points_resolved ? r.points - r.points_resolved : nil})
-
517
series.merge(:to_accept => series.collect{|a| a.points && a.points_accepted ? a.points - a.points_accepted : nil})
-
-
517
series.merge(:days_left => (0..@days.size).collect{|d| @days.size - d})
-
end
-
-
26
@data = {}
-
-
517
@data[:hours_remaining] = series.collect{|s| s.hours }
-
517
@data[:points_committed] = series.collect{|s| s.points }
-
517
@data[:points_accepted] = series.collect{|s| s.points_accepted }
-
517
@data[:points_resolved] = series.collect{|s| s.points_resolved }
-
517
@data[:points_to_resolve] = series.collect{|s| s.to_resolve }
-
517
@data[:points_to_accept] = series.collect{|s| s.to_accept }
-
-
26
@data[:ideal] = (0..@days.size).to_a.reverse
-
-
517
@data[:points_required_burn_rate] = series.collect{|r| r.to_resolve ? Float(r.to_resolve) / (r.days_left == 0 ? 1 : r.days_left) : nil }
-
517
@data[:hours_required_burn_rate] = series.collect{|r| r.hours ? Float(r.hours) / (r.days_left == 0 ? 1 : r.days_left) : nil }
-
-
26
case direction
-
when 'up'
-
10
@data.delete(:points_to_resolve)
-
10
@data.delete(:points_to_accept)
-
when 'down'
-
16
@data.delete(:points_resolved)
-
16
@data.delete(:points_accepted)
-
else
-
raise "Unexpected burn direction #{direction.inspect}"
-
end
-
end
-
-
1
def [](i)
-
292
i = i.intern if i.is_a?(String)
-
292
raise "No burn#{@direction} data series '#{i}', available: #{@data.keys.inspect}" unless @data[i]
-
292
return @data[i]
-
end
-
-
1
def series(remove_empty = true)
-
11
@series ||= {}
-
11
return @series[remove_empty] if @series[remove_empty]
-
-
64
@series[remove_empty] = @data.keys.collect{|k| k.to_s}.sort
-
8
return @series[remove_empty] unless remove_empty
-
-
# delete :points_committed if flatline
-
8
@series[remove_empty].delete('points_committed') if @data[:points_committed].uniq.compact.size < 1
-
-
# delete any series that is flat-line 0/nil
-
8
@series[remove_empty].each {|k|
-
1040
@series[remove_empty].delete(k) if k != 'points_committed' && @data[k.intern].collect{|d| d.to_f }.uniq == [0.0]
-
}
-
8
return @series[remove_empty]
-
end
-
-
1
attr_reader :days
-
1
attr_reader :sprint_id
-
1
attr_reader :data
-
1
attr_reader :direction
-
end
-
-
1
class RbSprint < Version
-
1
unloadable
-
-
1
validate :start_and_end_dates
-
-
1
def start_and_end_dates
-
415
errors.add_to_base("Sprint cannot end before it starts") if self.effective_date && self.sprint_start_date && self.sprint_start_date >= self.effective_date
-
end
-
-
1
def self.rb_scope(symbol, func)
-
2
if Rails::VERSION::MAJOR < 3
-
named_scope symbol, func
-
else
-
2
scope symbol, func
-
end
-
end
-
-
1
rb_scope :open_sprints, lambda { |project|
-
{
-
:order => "CASE sprint_start_date WHEN NULL THEN 1 ELSE 0 END ASC,
-
sprint_start_date ASC,
-
CASE effective_date WHEN NULL THEN 1 ELSE 0 END ASC,
-
effective_date ASC",
-
:conditions => [ "status = 'open' and project_id = ?", project.id ] #FIXME locked, too?
-
50
}
-
}
-
-
#TIB ajout du scope :closed_sprints
-
1
rb_scope :closed_sprints, lambda { |project|
-
{
-
:order => "CASE sprint_start_date WHEN NULL THEN 1 ELSE 0 END ASC,
-
sprint_start_date ASC,
-
CASE effective_date WHEN NULL THEN 1 ELSE 0 END ASC,
-
effective_date ASC",
-
:conditions => [ "status = 'closed' and project_id = ?", project.id ]
-
32
}
-
}
-
-
#depending on sharing mode
-
#return array of projects where this sprint is visible
-
1
def shared_to_projects(scope_project)
-
32
projects = []
-
32
Project.visible.find(:all, :order => 'lft').each{|_project| #exhaustive search FIXME (pa sharing)
-
1490
projects << _project unless (_project.shared_versions.collect{|v| v.id} & [id]).empty?
-
}
-
32
projects
-
end
-
-
1
def stories
-
334
return RbStory.sprint_backlog(self)
-
end
-
-
1
def points
-
return stories.inject(0){|sum, story| sum + story.story_points.to_i}
-
end
-
-
1
def has_wiki_page
-
1
return false if wiki_page_title.blank?
-
-
page = project.wiki.find_page(self.wiki_page_title)
-
return false if !page
-
-
template = find_wiki_template
-
return false if template && page.text == template.text
-
-
return true
-
end
-
-
1
def find_wiki_template
-
2
projects = [self.project] + self.project.ancestors
-
-
2
template = Backlogs.setting[:wiki_template]
-
2
if template =~ /:/
-
p, template = *template.split(':', 2)
-
projects << Project.find(p)
-
end
-
-
2
projects.compact!
-
-
2
projects.each{|p|
-
2
next unless p.wiki
-
2
t = p.wiki.find_page(template)
-
2
return t if t
-
}
-
return nil
-
end
-
-
1
def wiki_page
-
2
if ! project.wiki
-
return ''
-
end
-
-
2
self.update_attribute(:wiki_page_title, Wiki.titleize(self.name)) if wiki_page_title.blank?
-
-
2
page = project.wiki.find_page(self.wiki_page_title)
-
2
if !page
-
2
template = find_wiki_template
-
2
if template
-
2
page = WikiPage.new(:wiki => project.wiki, :title => self.wiki_page_title)
-
2
page.content = WikiContent.new
-
2
page.content.text = "h1. #{self.name}\n\n#{template.text}"
-
2
page.save!
-
end
-
end
-
-
2
return wiki_page_title
-
end
-
-
1
def days(cutoff)
-
462
return nil unless self.effective_date && self.sprint_start_date
-
-
462
case cutoff
-
when :active
-
396
d = (self.sprint_start_date .. [self.effective_date, Date.today].min)
-
when :all
-
66
d = (self.sprint_start_date .. self.effective_date)
-
else
-
raise "Unexpected day range '#{cutoff.inspect}'"
-
end
-
-
462
if Backlogs.setting[:include_sat_and_sun]
-
360
return d.to_a
-
else
-
# mon-fri are working days, sat-sun are not
-
883
return d.select {|d| (d.wday > 0 and d.wday < 6) }
-
end
-
end
-
-
1
def eta
-
return nil if ! self.start_date
-
-
dpp = self.project.scrum_statistics.info[:average_days_per_point]
-
return nil if !dpp
-
-
derived_days = if Backlogs.setting[:include_sat_and_sun]
-
Integer(self.points * dpp)
-
else
-
# 5 out of 7 are working days
-
Integer(self.points * dpp * 7.0/5)
-
end
-
return self.start_date + derived_days
-
end
-
-
1
def has_burndown?
-
292
return (days(:active) || []).size != 0
-
end
-
-
1
def activity
-
bd = self.burndown('up')
-
return false if !bd
-
-
# assume a sprint is active if it's only 2 days old
-
return true if bd[:hours_remaining].compact.size <= 2
-
-
return Issue.exists?(['fixed_version_id = ? and ((updated_on between ? and ?) or (created_on between ? and ?))', self.id, -2.days.from_now, Time.now, -2.days.from_now, Time.now])
-
end
-
-
1
def burndown(direction=nil)
-
37
return nil if not self.has_burndown?
-
-
37
direction ||= Backlogs.setting[:points_burn_direction]
-
37
direction = 'down' if direction != 'up'
-
-
37
@burndown ||= {'up' => nil, 'down' => nil}
-
37
@burndown[direction] ||= Burndown.new(self, direction)
-
37
return @burndown[direction]
-
end
-
-
1
def impediments
-
@impediments ||= Issue.find(:all,
-
:conditions => ["id in (
-
select issue_from_id
-
from issue_relations ir
-
join issues blocked
-
on blocked.id = ir.issue_to_id
-
and blocked.tracker_id in (?)
-
and blocked.fixed_version_id = (?)
-
where ir.relation_type = 'blocks'
-
)",
-
RbStory.trackers + [RbTask.tracker],
-
self.id]
-
268
) #.sort {|a,b| a.closed? == b.closed? ? a.updated_on <=> b.updated_on : (a.closed? ? 1 : -1) }
-
end
-
-
end
-
1
class RbStory < Issue
-
1
unloadable
-
-
1
def self.find_options(options)
-
574
options = options.dup
-
-
574
project = options.delete(:project)
-
574
if project.nil?
-
project_id = nil
-
elsif project.is_a?(Integer)
-
468
project_id = project
-
468
project = nil
-
else
-
106
project_id = project.id
-
end
-
-
574
sprint_ids = options.delete(:sprint)
-
574
sprint_ids = [sprint_ids] if sprint_ids && !sprint_ids.is_a?(Array)
-
1123
sprint_ids = sprint_ids.collect{|s| s.is_a?(Integer) ? s : s.id} if sprint_ids
-
-
574
permission = options.delete(:permission)
-
574
permission = false if permission.nil?
-
-
574
options[:conditions] ||= []
-
-
574
if permission
-
if Issue.respond_to? :visible_condition
-
visible = Issue.visible_condition(User.current, :project => project || Project.find(project_id))
-
else
-
visible = Project.allowed_to_condition(User.current, :view_issues)
-
end
-
Backlogs::ActiveRecord.add_condition(options, visible)
-
end
-
-
574
if Backlogs.settings[:sharing_enabled]
-
429
pbl_condition = ["
-
(#{Project.find(project_id).project_condition(true)})
-
and tracker_id in (?)
-
and fixed_version_id is NULL
-
and is_closed = ?", RbStory.trackers, false]
-
429
sprint_condition = ["
-
tracker_id in (?)
-
and fixed_version_id IN (?)", RbStory.trackers, sprint_ids]
-
else
-
145
pbl_condition = ["
-
project_id = ?
-
and tracker_id in (?)
-
and fixed_version_id is NULL
-
and is_closed = ?", project_id, RbStory.trackers, false]
-
145
sprint_condition = ["
-
project_id = ?
-
and tracker_id in (?)
-
and fixed_version_id IN (?)", project_id, RbStory.trackers, sprint_ids]
-
end
-
-
574
if sprint_ids.nil?
-
172
Backlogs::ActiveRecord.add_condition(options, pbl_condition)
-
172
options[:joins] ||= []
-
172
options[:joins] [options[:joins]] unless options[:joins].is_a?(Array)
-
172
options[:joins] << :status
-
172
options[:joins] << :project
-
else
-
402
Backlogs::ActiveRecord.add_condition(options, sprint_condition)
-
end
-
-
574
return options
-
end
-
-
1
def self.backlog(project_id, sprint_id, options={})
-
463
stories = []
-
-
463
prev = nil
-
463
RbStory.find(:all, RbStory.find_options(options.merge({
-
:project => project_id,
-
:sprint => sprint_id,
-
:order => :position,
-
}))).each_with_index {|story, i|
-
1066
stories << story
-
-
1066
prev.higher_item = story if prev
-
1066
story.lower_item = prev
-
-
1066
story.rank = i + 1
-
-
1066
prev = story
-
}
-
-
463
return stories
-
end
-
-
1
def self.product_backlog(project, limit=nil)
-
65
return RbStory.backlog(project.id, nil, :limit => limit)
-
end
-
-
1
def self.sprint_backlog(sprint, options={})
-
334
return RbStory.backlog(sprint.project.id, sprint.id, options)
-
end
-
-
1
def self.backlogs_by_sprint(project, sprints, options={})
-
273
ret = RbStory.backlog(project.id, sprints.map {|s| s.id }, options)
-
63
sprint_of = {}
-
63
ret.each do |backlog|
-
154
sprint_of[backlog.fixed_version_id] ||= []
-
154
sprint_of[backlog.fixed_version_id].push(backlog)
-
end
-
63
return sprint_of
-
end
-
-
1
def self.stories_open(project)
-
7
stories = []
-
-
7
RbStory.find(:all,
-
:order => :position,
-
:conditions => ["project_id = ? AND tracker_id in (?) and is_closed = ?",project.id,RbStory.trackers,false],
-
:joins => :status).each_with_index {|story, i|
-
42
story.rank = i + 1
-
42
stories << story
-
}
-
7
return stories
-
end
-
-
1
def self.create_and_position(params)
-
761
params['prev'] = params.delete('prev_id') if params.include?('prev_id')
-
761
params['next'] = params.delete('next_id') if params.include?('next_id')
-
-
# lft and rgt fields are handled by acts_as_nested_set
-
20549
attribs = params.select{|k,v| !['prev', 'id', 'lft', 'rgt'].include?(k) && RbStory.column_names.include?(k) }
-
761
attribs = Hash[*attribs.flatten]
-
761
s = RbStory.new(attribs)
-
761
s.save!
-
761
s.position!(params)
-
761
return s
-
end
-
-
1
def self.find_all_updated_since(since, project_id)
-
2
find(:all,
-
:conditions => ["project_id = ? AND updated_on > ? AND tracker_id in (?)", project_id, Time.parse(since), trackers],
-
:order => "updated_on ASC")
-
end
-
-
1
def self.trackers(options = {})
-
# legacy
-
10570
options = {:type => options} if options.is_a?(Symbol)
-
-
# somewhere early in the initialization process during first-time migration this gets called when the table doesn't yet exist
-
10570
trackers = []
-
10570
if ActiveRecord::Base.connection.tables.include?('settings')
-
10570
trackers = Backlogs.setting[:story_trackers]
-
10570
trackers = [] if trackers.blank?
-
end
-
-
10570
trackers = Tracker.find_all_by_id(trackers)
-
10570
trackers = trackers & options[:project].trackers if options[:project]
-
21140
trackers = trackers.sort_by { |t| [t.position] }
-
-
10570
case options[:type]
-
63
when :trackers then return trackers
-
20978
when :array, nil then return trackers.collect{|t| t.id}
-
36
when :string then return trackers.collect{|t| t.id.to_s}.join(',')
-
else raise "Unexpected return type #{options[:type].inspect}"
-
end
-
end
-
-
1
def tasks
-
47
return RbTask.tasks_for(self.id)
-
end
-
-
1
def set_points(p)
-
return self.journalized_update_attribute(:story_points, nil) if p.blank? || p == '-'
-
-
return self.journalized_update_attribute(:story_points, 0) if p.downcase == 's'
-
-
return self.journalized_update_attribute(:story_points, Float(p)) if Float(p) >= 0
-
end
-
-
1
def points_display(notsized='-')
-
# For reasons I have yet to uncover, activerecord will
-
# sometimes return numbers as Fixnums that lack the nil?
-
# method. Comparing to nil should be safe.
-
2
return notsized if story_points == nil || story_points.blank?
-
return 'S' if story_points == 0
-
return story_points.to_s
-
end
-
-
1
def update_and_position!(params)
-
33
params['prev'] = params.delete('prev_id') if params.include?('prev_id')
-
33
params['next'] = params.delete('next_id') if params.include?('next_id')
-
33
self.position!(params)
-
-
# lft and rgt fields are handled by acts_as_nested_set
-
467
attribs = params.select{|k,v| !['prev', 'id', 'project_id', 'lft', 'rgt'].include?(k) && RbStory.column_names.include?(k) }
-
33
attribs = Hash[*attribs.flatten]
-
-
33
return self.journalized_update_attributes attribs
-
end
-
-
1
def position!(params)
-
794
if params.include?('prev')
-
8
if params['prev'].blank?
-
6
self.move_to_top
-
else
-
2
self.move_after(RbStory.find(params['prev']))
-
end
-
786
elsif params.include?('next')
-
23
if params['next'].blank?
-
2
self.move_to_bottom
-
else
-
21
self.move_before(RbStory.find(params['next']))
-
end
-
end
-
end
-
-
1
def burndown(sprint=nil)
-
61
return nil unless self.is_story?
-
61
sprint ||= self.fixed_version.becomes(RbSprint) if self.fixed_version
-
61
return nil if sprint.nil? || !sprint.has_burndown?
-
-
61
return Rails.cache.fetch("RbIssue(#{self.id}@#{self.updated_on}).burndown(#{sprint.id}@#{sprint.updated_on}-#{[Date.today, sprint.effective_date].min})") {
-
46
bd = {}
-
-
46
if sprint.has_burndown?
-
46
days = sprint.days(:active)
-
-
46
series = Backlogs::MergedArray.new
-
621
series.merge(:in_sprint => history(:fixed_version_id, days).collect{|s| s == sprint.id})
-
46
series.merge(:points => history(:story_points, days))
-
46
series.merge(:open => history(:status_open, days))
-
46
series.merge(:accepted => history(:status_success, days))
-
46
series.merge(:hours => ([0] * (days.size + 1)))
-
-
78
tasks.each{|task| series.add(:hours => task.burndown(sprint)) }
-
-
46
series.each {|datapoint|
-
575
if datapoint.in_sprint
-
272
datapoint.hours = 0 unless datapoint.open
-
272
datapoint.points_accepted = (datapoint.accepted ? datapoint.points : nil)
-
272
datapoint.points_resolved = (datapoint.accepted || datapoint.hours.to_f == 0.0 ? datapoint.points : nil)
-
else
-
303
datapoint.nilify
-
303
datapoint.points_accepted = nil
-
303
datapoint.points_resolved = nil
-
end
-
}
-
-
# collect points on this sprint
-
46
bd[:points] = series.series(:points)
-
46
bd[:points_accepted] = series.series(:points_accepted)
-
46
bd[:points_resolved] = series.series(:points_resolved)
-
621
bd[:hours] = series.collect{|datapoint| datapoint.open ? datapoint.hours : nil}
-
end
-
-
46
bd
-
}
-
end
-
-
1
def rank
-
6
return super(RbStory.find_options(:project => self.project_id, :sprint => self.fixed_version_id))
-
end
-
-
end
-
1
require 'date'
-
-
1
class RbTask < Issue
-
1
unloadable
-
-
1
def self.tracker
-
9122
task_tracker = Backlogs.setting[:task_tracker]
-
9122
return nil if task_tracker.blank?
-
9122
return Integer(task_tracker)
-
end
-
-
1
def self.rb_safe_attributes(params)
-
236
if Issue.const_defined? "SAFE_ATTRIBUTES"
-
safe_attributes_names = RbTask::SAFE_ATTRIBUTES
-
else
-
236
safe_attributes_names = Issue.new(
-
:project_id=>params[:project_id] # required to verify "safeness"
-
).safe_attribute_names
-
end
-
6550
attribs = params.select {|k,v| safe_attributes_names.include?(k) }
-
# lft and rgt fields are handled by acts_as_nested_set
-
4274
attribs = attribs.select{|k,v| k != 'lft' and k != 'rgt' }
-
236
attribs = Hash[*attribs.flatten] if attribs.is_a?(Array)
-
236
return attribs
-
end
-
-
1
def self.create_with_relationships(params, user_id, project_id, is_impediment = false)
-
231
attribs = rb_safe_attributes(params)
-
-
231
attribs['author_id'] = user_id
-
231
attribs['tracker_id'] = RbTask.tracker
-
231
attribs['project_id'] = project_id
-
-
231
blocks = params.delete('blocks')
-
-
#if we are an impediment and have blocks, set our project_id.
-
#if we have multiple blocked tasks, cross-project relations must be enabled, otherwise save-validation will fail. TODO: make this more user friendly by pre-validating here and suggesting to enable cross-project relation support in redmine base setup.
-
231
if is_impediment and blocks and blocks.strip != ''
-
103
begin
-
103
first_blocked_id = blocks.split(/\D+/)[0].to_i
-
103
attribs['project_id'] = Issue.find_by_id(first_blocked_id).project_id if first_blocked_id
-
rescue
-
end
-
end
-
-
231
task = new(attribs)
-
231
if params['parent_issue_id']
-
128
parent = Issue.find(params['parent_issue_id'])
-
128
task.start_date = parent.start_date
-
end
-
231
task.save!
-
-
231
raise "Block list must be comma-separated list of task IDs" if is_impediment && !task.validate_blocks_list(blocks) # could we do that before save and integrate cross-project checks?
-
-
231
task.move_before params[:next] unless is_impediment # impediments are not hosted under a single parent, so you can't tree-order them
-
231
task.update_blocked_list blocks.split(/\D+/) if is_impediment
-
230
task.time_entry_add(params)
-
-
230
return task
-
end
-
-
# TODO: there's an assumption here that impediments always have the
-
# task-tracker as their tracker, and are top-level issues.
-
1
def self.find_all_updated_since(since, project_id, find_impediments = false)
-
6
find(:all,
-
:conditions => ["project_id = ? AND updated_on > ? AND tracker_id in (?) and parent_id IS #{ find_impediments ? '' : 'NOT' } NULL", project_id, Time.parse(since), tracker],
-
:order => "updated_on ASC")
-
end
-
-
1
def self.tasks_for(story_id)
-
47
tasks = []
-
47
story = RbStory.find_by_id(story_id)
-
47
if RbStory.trackers.include?(story.tracker_id)
-
47
story.descendants.each_with_index {|task, i|
-
34
task = task.becomes(RbTask)
-
34
task.rank = i + 1
-
34
tasks << task
-
}
-
end
-
47
return tasks
-
end
-
-
1
def update_with_relationships(params, is_impediment = false)
-
5
time_entry_add(params)
-
-
5
attribs = RbTask.rb_safe_attributes(params)
-
-
# Auto assign task to current user when
-
# 1. the task is not assigned to anyone yet
-
# 2. task status changed (i.e. Updating task name or remaining hours won't assign task to user)
-
# Can be enabled/disabled in setting page
-
5
if Backlogs.setting[:auto_assign_task] && self.assigned_to_id.blank? && (self.status_id != params[:status_id].to_i)
-
attribs[:assigned_to_id] = User.current.id
-
end
-
-
5
valid_relationships = if is_impediment && params[:blocks] #if blocks param was not sent, that means the impediment was just dragged
-
validate_blocks_list(params[:blocks])
-
else
-
5
true
-
end
-
-
5
if valid_relationships && result = self.journalized_update_attributes!(attribs)
-
5
move_before params[:next] unless is_impediment # impediments are not hosted under a single parent, so you can't tree-order them
-
5
update_blocked_list params[:blocks].split(/\D+/) if params[:blocks]
-
-
5
if params.has_key?(:remaining_hours)
-
3
begin
-
3
self.remaining_hours = Float(params[:remaining_hours].to_s.gsub(',', '.'))
-
rescue ArgumentError, TypeError
-
3
Rails.logger.warn "#{params[:remaining_hours]} is wrong format for remaining hours."
-
end
-
3
sprint_start = self.story.fixed_version.becomes(RbSprint).sprint_start_date if self.story
-
3
self.estimated_hours = self.remaining_hours if (sprint_start == nil) || (Date.today < sprint_start)
-
3
save
-
end
-
-
5
result
-
else
-
false
-
end
-
end
-
-
1
def update_blocked_list(for_blocking)
-
# Existing relationships not in for_blocking should be removed from the 'blocks' list
-
104
relations_from.find(:all, :conditions => "relation_type='blocks'").each{ |ir|
-
1
ir.destroy unless for_blocking.include?( ir[:issue_to_id] )
-
}
-
-
104
already_blocking = relations_from.find(:all, :conditions => "relation_type='blocks'").map{|ir| ir.issue_to_id}
-
-
# Non-existing relationships that are in for_blocking should be added to the 'blocks' list
-
210
for_blocking.select{ |id| !already_blocking.include?(id) }.each{ |id|
-
106
ir = relations_from.new(:relation_type=>'blocks')
-
106
ir[:issue_to_id] = id
-
106
ir.save!
-
}
-
103
reload
-
end
-
-
1
def validate_blocks_list(list)
-
103
if list.split(/\D+/).length==0
-
errors.add :blocks, :must_have_comma_delimited_list
-
false
-
else
-
103
true
-
end
-
end
-
-
# assumes the task is already under the same story as 'id'
-
1
def move_before(id)
-
133
id = nil if id.respond_to?('blank?') && id.blank?
-
133
if id.nil?
-
133
sib = self.siblings
-
133
move_to_right_of sib[-1].id if sib.any?
-
else
-
move_to_left_of id
-
end
-
end
-
-
1
def rank=(r)
-
34
@rank = r
-
end
-
-
1
def rank
-
s = self.story
-
return nil if !s
-
-
@rank ||= Issue.count( :conditions => ['tracker_id = ? and root_id = ? and lft > ? and lft <= ?', RbTask.tracker, s.root_id, s.lft, self.lft])
-
return @rank
-
end
-
-
1
def burndown(sprint = nil)
-
32
return nil unless self.is_task?
-
32
sprint ||= self.fixed_version.becomes(RbSprint) if self.fixed_version
-
32
return nil if sprint.nil? || !sprint.has_burndown?
-
-
32
return Rails.cache.fetch("RbIssue(#{self.id}@#{self.updated_on}).burndown(#{sprint.id}@#{sprint.updated_on}-#{[Date.today, sprint.effective_date].min})") {
-
32
days = sprint.days(:active)
-
-
32
earliest_estimate = history(:estimated_hours, days).compact[0]
-
-
32
series = Backlogs::MergedArray.new
-
32
series.merge(:hours => history(:remaining_hours, days))
-
32
series.merge(:sprint => history(:fixed_version_id, days))
-
32
series.each_with_index{|d, i|
-
262
if d.sprint != sprint.id
-
6
d.hours = nil
-
elsif i == 0 && d.hours.to_f == 0 && earliest_estimate.to_f != 0.0
-
# set hours to earliest estimate *within sprint* if first day is not filled out
-
d.hours = earliest_estimate
-
end
-
}
-
32
series.series(:hours)
-
}
-
end
-
-
1
def time_entry_add(params)
-
# Will also save time entry if only comment is filled, hours will default to 0. We don't want the user
-
# to loose a precious comment if hours is accidently left blank.
-
235
if !params[:time_entry_hours].blank? || !params[:time_entry_comments].blank?
-
@time_entry = TimeEntry.new(:issue => self, :project => self.project)
-
# Make sure user has permission to edit time entries to allow
-
# logging time for other users. Use current user in case none is selected
-
if User.current.allowed_to?(:edit_time_entries, self.project) && params[:time_entry_user_id].to_i != 0
-
@time_entry.user_id = params[:time_entry_user_id]
-
else
-
# Otherwise log time for current user
-
@time_entry.user_id = User.current.id
-
end
-
if !params[:time_entry_spent_on].blank?
-
@time_entry.spent_on = params[:time_entry_spent_on]
-
else
-
@time_entry.spent_on = Date.today
-
end
-
@time_entry.hours = params[:time_entry_hours].gsub(',', '.').to_f
-
# Choose default activity
-
# If default is not defined first activity will be chosen
-
if default_activity = TimeEntryActivity.default
-
@time_entry.activity_id = default_activity.id
-
else
-
@time_entry.activity_id = TimeEntryActivity.first.id
-
end
-
@time_entry.comments = params[:time_entry_comments]
-
self.time_entries << @time_entry
-
end
-
end
-
end
-
1
class ReleaseBurndownDay < ActiveRecord::Base
-
1
unloadable
-
1
belongs_to :rb_release, :foreign_key => :release_id
-
end
-
1
require 'redmine'
-
-
1
if Rails::VERSION::MAJOR < 3
-
require 'dispatcher'
-
object_to_prepare = Dispatcher
-
else
-
1
object_to_prepare = Rails.configuration
-
# if redmine plugins were railties:
-
# object_to_prepare = config
-
end
-
1
object_to_prepare.to_prepare do
-
1
require_dependency 'backlogs_activerecord_mixin'
-
1
require_dependency 'backlogs_setup'
-
1
require_dependency 'issue'
-
-
1
if Issue.const_defined? "SAFE_ATTRIBUTES"
-
Issue::SAFE_ATTRIBUTES << "story_points"
-
Issue::SAFE_ATTRIBUTES << "position"
-
Issue::SAFE_ATTRIBUTES << "remaining_hours"
-
else
-
1
Issue.safe_attributes "story_points", "position", "remaining_hours"
-
end
-
-
1
require_dependency 'backlogs_query_patch'
-
1
require_dependency 'backlogs_issue_patch'
-
1
require_dependency 'backlogs_issue_status_patch'
-
1
require_dependency 'backlogs_tracker_patch'
-
1
require_dependency 'backlogs_version_patch'
-
1
require_dependency 'backlogs_project_patch'
-
1
require_dependency 'backlogs_user_patch'
-
1
require_dependency 'backlogs_journal_patch'
-
1
require_dependency 'backlogs_custom_field_patch'
-
-
1
require_dependency 'backlogs_my_controller_patch'
-
1
require_dependency 'backlogs_issues_controller_patch'
-
-
1
require_dependency 'backlogs_hooks'
-
-
1
require_dependency 'backlogs_merged_array'
-
-
1
require_dependency 'backlogs_printable_cards'
-
-
1
Redmine::AccessControl.permission(:manage_versions).actions << "rb_sprints/close_completed"
-
end
-
-
-
1
Redmine::Plugin.register :redmine_backlogs do
-
1
name 'Redmine Backlogs'
-
1
author "friflaj,Mark Maglana,John Yani,mikoto20000,Frank Blendinger,Bo Hansen,stevel"
-
1
description 'A plugin for agile teams'
-
1
version 'v0.9.26'
-
-
settings :default => {
-
:story_trackers => nil,
-
:task_tracker => nil,
-
:card_spec => nil,
-
:taskboard_card_order => 'story_follows_tasks',
-
:story_points => "1,2,3,5,8",
-
:show_burndown_in_sidebar => 'enabled'
-
},
-
1
:partial => 'backlogs/settings'
-
-
1
project_module :backlogs do
-
# SYNTAX: permission :name_of_permission, { :controller_name => [:action1, :action2] }
-
-
# Master backlog permissions
-
1
permission :reset_sprint, {
-
:rb_sprints => :reset
-
}
-
1
permission :view_master_backlog, {
-
:rb_master_backlogs => [:show, :menu],
-
:rb_sprints => [:index, :show, :download],
-
:rb_hooks_render => [:view_issues_sidebar],
-
:rb_wikis => :show,
-
:rb_stories => [:index, :show],
-
:rb_queries => [:show, :impediments],
-
:rb_server_variables => [:project, :sprint, :index],
-
:rb_burndown_charts => [:embedded, :show, :print],
-
:rb_updated_items => :show
-
}
-
-
1
permission :view_releases, {
-
:rb_releases => [:index, :show],
-
:rb_sprints => [:index, :show, :download],
-
:rb_wikis => :show,
-
:rb_stories => [:index, :show],
-
:rb_server_variables => [:project, :sprint, :index],
-
:rb_burndown_charts => [:embedded, :show, :print],
-
:rb_updated_items => :show
-
}
-
-
1
permission :view_taskboards, {
-
:rb_taskboards => :show,
-
:rb_sprints => :show,
-
:rb_stories => [:index, :show],
-
:rb_tasks => [:index, :show],
-
:rb_impediments => [:index, :show],
-
:rb_wikis => :show,
-
:rb_server_variables => [:project, :sprint, :index],
-
:rb_hooks_render => [:view_issues_sidebar],
-
:rb_burndown_charts => [:embedded, :show, :print],
-
:rb_updated_items => :show
-
}
-
-
# Release permissions
-
1
permission :modify_releases, { :rb_releases => [:new, :create, :edit, :snapshot, :destroy] }
-
-
# Sprint permissions
-
# :show_sprints and :list_sprints are implicit in :view_master_backlog permission
-
1
permission :create_sprints, { :rb_sprints => [:new, :create] }
-
1
permission :update_sprints, {
-
:rb_sprints => [:edit, :update],
-
:rb_wikis => [:edit, :update]
-
}
-
-
# Story permissions
-
# :show_stories and :list_stories are implicit in :view_master_backlog permission
-
1
permission :create_stories, { :rb_stories => :create }
-
1
permission :update_stories, { :rb_stories => :update }
-
-
# Task permissions
-
# :show_tasks and :list_tasks are implicit in :view_sprints
-
1
permission :create_tasks, { :rb_tasks => [:new, :create] }
-
1
permission :update_tasks, { :rb_tasks => [:edit, :update] }
-
-
1
permission :update_remaining_hours, { :rb_tasks => [:edit, :update] }
-
-
# Impediment permissions
-
# :show_impediments and :list_impediments are implicit in :view_sprints
-
1
permission :create_impediments, { :rb_impediments => [:new, :create] }
-
1
permission :update_impediments, { :rb_impediments => [:edit, :update] }
-
-
1
permission :subscribe_to_calendars, { :rb_calendars => :ical }
-
1
permission :view_scrum_statistics, { :rb_all_projects => :statistics }
-
end
-
-
109
menu :project_menu, :rb_master_backlogs, { :controller => :rb_master_backlogs, :action => :show }, :caption => :label_backlogs, :after => :issues, :param => :project_id, :if => Proc.new { Backlogs.configured? }
-
109
menu :project_menu, :rb_releases, { :controller => :rb_releases, :action => :index }, :caption => :label_release_plural, :after => :rb_master_backlogs, :param => :project_id, :if => Proc.new { Backlogs.configured? }
-
252
menu :application_menu, :rb_statistics, { :controller => :rb_all_projects, :action => :statistics}, :caption => :label_scrum_statistics, :if => Proc.new { Backlogs.configured? && User.current.allowed_to?({:controller => :rb_all_projects, :action => :statistics}, nil, :global => true) }
-
end
-
1
module Backlogs
-
1
module ActiveRecord
-
1
def add_condition(options, condition, conjunction = 'AND')
-
775
if condition.is_a? String
-
add_condition(options, [condition], conjunction)
-
775
elsif condition.is_a? Hash
-
add_condition!(options, [condition.keys.map { |attr| "#{attr}=?" }.join(' AND ')] + condition.values, conjunction)
-
775
elsif condition.is_a? Array
-
775
options[:conditions] ||= []
-
775
options[:conditions][0] += " #{conjunction} (#{condition.shift})" unless options[:conditions].empty?
-
775
options[:conditions] = options[:conditions] + condition
-
else
-
raise "don't know how to handle this condition type"
-
end
-
end
-
1
module_function :add_condition
-
-
1
module Attributes
-
1
def self.included receiver
-
4
receiver.extend ClassMethods
-
end
-
-
1
module ClassMethods
-
1
def rb_sti_class
-
36912
return self.ancestors.select{|klass| klass.name !~ /^Rb/ && klass.ancestors.include?(::ActiveRecord::Base)}[0]
-
end
-
end
-
-
1
def available_custom_fields
-
422
klass = self.class.respond_to?(:rb_sti_class) ? self.class.rb_sti_class : self.class
-
422
CustomField.find(:all, :conditions => "type = '#{klass.name}CustomField'", :order => 'position')
-
end
-
-
1
def journalized_update_attributes!(attribs)
-
5
self.init_journal(User.current)
-
5
return self.update_attributes!(attribs)
-
end
-
1
def journalized_update_attributes(attribs)
-
33
self.init_journal(User.current)
-
33
return self.update_attributes(attribs)
-
end
-
1
def journalized_update_attribute(attrib, v)
-
1
self.init_journal(User.current)
-
1
return self.update_attribute(attrib, v)
-
end
-
end
-
-
1
module ListWithGaps
-
1
def self.included(base)
-
1
base.extend(ClassMethods)
-
end
-
-
1
module ClassMethods
-
1
def acts_as_list_with_gaps(options={})
-
1
options[:spacing] ||= 50
-
1
options[:default] ||= :top
-
-
class_eval <<-EOV
-
include Backlogs::ActiveRecord::ListWithGaps::InstanceMethods
-
-
def self.list_spacing
-
#{options[:spacing]}
-
end
-
-
def self.find_by_rank(r, options)
-
self.find(:first, options.merge(:order => '#{self.table_name}.position', :limit => 1, :offset => r - 1))
-
end
-
-
before_create :move_to_#{options[:default]}
-
1
EOV
-
end
-
end
-
-
1
module InstanceMethods
-
1
def move_to_top(options={})
-
1999
top = self.class.minimum(:position)
-
1999
return if self.position == top && !top.blank?
-
1999
self.position = top.blank? ? 0 : (top - self.class.list_spacing)
-
1999
list_commit
-
end
-
-
1
def move_to_bottom(options={})
-
763
bottom = self.class.maximum(:position)
-
763
return if self.position == bottom && !bottom.blank?
-
686
self.position = bottom.blank? ? 0 : (bottom + self.class.list_spacing)
-
686
list_commit
-
end
-
-
1
def first(options = {})
-
return self.class.find_by_position(self.class.minimum(:position, options))
-
end
-
-
1
def last(options = {})
-
return self.class.find_by_position(self.class.maximum(:position, options))
-
end
-
-
1
def higher_item(options = {})
-
932
@higher_item ||= list_prev_next(:prev, options)
-
end
-
1
attr_writer :higher_item
-
-
1
def lower_item(options = {})
-
22
@lower_item ||= list_prev_next(:next, options)
-
end
-
1
attr_writer :lower_item
-
-
1
def rank(options={})
-
6
options = options.dup
-
6
Backlogs::ActiveRecord.add_condition(options, ["#{self.class.table_name}.position <= ?", self.position])
-
6
@rank ||= self.class.count(options)
-
end
-
1
attr_writer :rank
-
-
1
def move_after(reference, options={})
-
22
nxt = reference.lower_item
-
-
22
if nxt.blank?
-
1
move_to_bottom
-
else
-
21
if (nxt.position - reference.position) < 2
-
2
self.class.connection.execute("update #{self.class.table_name} set position = position + #{self.class.list_spacing} where position >= #{nxt.position}")
-
2
nxt.position += self.class.list_spacing
-
end
-
21
self.position = (nxt.position + reference.position) / 2
-
end
-
-
22
list_commit
-
end
-
-
1
def move_before(reference, options={})
-
21
prev = reference.higher_item
-
21
if prev.blank?
-
1
move_to_top
-
else
-
20
move_after(prev, options)
-
end
-
end
-
end
-
-
1
private
-
-
1
def list_commit
-
2707
self.class.connection.execute("update #{self.class.table_name} set position = #{self.position} where id = #{self.id}") unless self.new_record?
-
end
-
-
1
def list_prev_next(dir, options)
-
258
return nil if self.new_record?
-
195
raise "#{self.class}##{self.id}: cannot request #{dir} for nil position" unless self.position
-
195
options = options.dup
-
195
Backlogs::ActiveRecord.add_condition(options, ["#{self.class.table_name}.position #{dir == :prev ? '<' : '>'} ?", self.position])
-
195
options[:order] = "#{self.class.table_name}.position #{dir == :prev ? 'desc' : 'asc'}"
-
195
return self.class.find(:first, options)
-
end
-
-
end
-
end
-
end
-
-
1
ActiveRecord::Base.send(:include, Backlogs::ActiveRecord::ListWithGaps) unless ActiveRecord::Base.included_modules.include? Backlogs::ActiveRecord::ListWithGaps
-
1
require_dependency 'custom_field'
-
-
1
module Backlogs
-
1
module CustomFieldPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
1
base.class_eval do
-
1
class << self
-
1
alias_method_chain :customized_class, :sti
-
end
-
end
-
end
-
-
1
module ClassMethods
-
1
def customized_class_with_sti
-
352
(self.respond_to?(:rb_sti_class) ? self.rb_sti_class : self).customized_class_without_sti
-
end
-
end
-
-
1
module InstanceMethods
-
end
-
end
-
end
-
-
1
CustomField.send(:include, Backlogs::CustomFieldPatch) unless CustomField.included_modules.include? Backlogs::CustomFieldPatch
-
1
module BacklogsPlugin
-
1
module Hooks
-
1
class LayoutHook < Redmine::Hook::ViewListener
-
# this ought to be view_issues_sidebar_queries_bottom, but
-
# the entire queries toolbar is disabled if you don't have
-
# custom queries
-
-
1
def exception(context, ex)
-
context[:controller].send(:flash)[:error] = "Backlogs error: #{ex.message} (#{ex.class})"
-
Rails.logger.error "#{ex.message} (#{ex.class}): " + ex.backtrace.join("\n")
-
end
-
-
1
def view_issues_sidebar_planning_bottom(context={ })
-
16
begin
-
16
project = context[:project]
-
-
16
return '' unless project && !project.blank?
-
16
return '' unless Backlogs.configured?(project)
-
-
16
sprint_id = nil
-
-
16
params = context[:controller].params
-
16
case "#{params['controller']}##{params['action']}"
-
when 'issues#show'
-
2
if params['id'] && (issue = Issue.find(params['id'])) && (issue.is_task? || issue.is_story?) && issue.fixed_version
-
2
sprint_id = issue.fixed_version_id
-
end
-
-
when 'issues#index'
-
14
q = context[:request].session[:query]
-
14
sprint = (q && q[:filters]) ? q[:filters]['fixed_version_id'] : nil
-
14
if sprint && sprint[:operator] == '=' && sprint[:values].size == 1
-
6
sprint_id = sprint[:values][0]
-
end
-
end
-
-
16
url_options = {
-
:only_path => true,
-
:controller => :rb_hooks_render,
-
:action => :view_issues_sidebar,
-
:project_id => project.identifier
-
}
-
16
url_options[:sprint_id] = sprint_id if sprint_id
-
16
if Rails::VERSION::MAJOR < 3
-
url = '' #actionpack-2.3.14/lib/action_controller/url_rewriter.rb is injecting relative_url_root
-
else
-
16
url = Redmine::Utils.relative_url_root #actionpack-3* is not???
-
end
-
16
url += url_for(url_options)
-
-
# Why can't I access protect_against_forgery?
-
return %{
-
<div id="backlogs_view_issues_sidebar"></div>
-
<script type="text/javascript">
-
jQuery(document).ready(function() {
-
16
jQuery('#backlogs_view_issues_sidebar').load('#{url}');
-
});
-
</script>
-
}
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_issues_show_details_bottom(context={ })
-
2
begin
-
2
issue = context[:issue]
-
-
2
return '' unless Backlogs.configured?(issue.project)
-
-
2
snippet = ''
-
-
2
project = context[:project]
-
-
2
if issue.is_story?
-
2
snippet += "<tr><th>#{l(:field_story_points)}</th><td>#{RbStory.find(issue.id).points_display}</td></tr>"
-
2
vbe = issue.velocity_based_estimate
-
2
snippet += "<tr><th>#{l(:field_velocity_based_estimate)}</th><td>#{vbe ? vbe.to_s + ' days' : '-'}</td></tr>"
-
end
-
-
2
if issue.is_task? && User.current.allowed_to?(:update_remaining_hours, project) != nil
-
snippet += "<tr><th>#{l(:field_remaining_hours)}</th><td>#{issue.remaining_hours}</td></tr>"
-
end
-
-
2
return snippet
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_issues_form_details_bottom(context={ })
-
4
begin
-
4
snippet = ''
-
4
issue = context[:issue]
-
-
4
return '' unless Backlogs.configured?(issue.project)
-
-
#project = context[:project]
-
-
#developers = project.members.select {|m| m.user.allowed_to?(:log_time, project)}.collect{|m| m.user}
-
#developers = select_tag("time_entry[user_id]", options_from_collection_for_select(developers, :id, :name, User.current.id))
-
#developers = developers.gsub(/\n/, '')
-
-
4
if issue.is_story?
-
4
snippet += '<p>'
-
#snippet += context[:form].label(:story_points)
-
4
snippet += context[:form].text_field(:story_points, :size => 3)
-
4
snippet += '</p>'
-
-
4
if issue.descendants.length != 0 && !issue.new_record?
-
1
snippet += javascript_include_tag 'jquery/jquery-1.6.2.min.js', :plugin => 'redmine_backlogs'
-
1
snippet += <<-generatedscript
-
-
<script type="text/javascript">
-
var $j = jQuery.noConflict();
-
-
$j(document).ready(function() {
-
$j('#issue_estimated_hours').attr('disabled', 'disabled');
-
$j('#issue_done_ratio').attr('disabled', 'disabled');
-
$j('#issue_start_date').parent().hide();
-
$j('#issue_due_date').parent().hide();
-
});
-
</script>
-
generatedscript
-
end
-
end
-
-
4
params = context[:controller].params
-
4
if issue.is_story? && params[:copy_from]
-
2
snippet += "<p><label for='link_to_original'>#{l(:rb_label_link_to_original)}</label>"
-
2
snippet += "#{check_box_tag('link_to_original', params[:copy_from], true)}</p>"
-
-
2
snippet += "<p><label>#{l(:rb_label_copy_tasks)}</label>"
-
2
snippet += "#{radio_button_tag('copy_tasks', 'open:' + params[:copy_from], true)} #{l(:rb_label_copy_tasks_open)}<br />"
-
2
snippet += "#{radio_button_tag('copy_tasks', 'none', false)} #{l(:rb_label_copy_tasks_none)}<br />"
-
2
snippet += "#{radio_button_tag('copy_tasks', 'all:' + params[:copy_from], false)} #{l(:rb_label_copy_tasks_all)}</p>"
-
end
-
-
4
if issue.is_task? && !issue.new_record?
-
snippet += "<p><label for='remaining_hours'>#{l(:field_remaining_hours)}</label>"
-
snippet += text_field_tag('remaining_hours', issue.remaining_hours, :size => 3)
-
snippet += '</p>'
-
end
-
-
4
return snippet
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_versions_show_bottom(context={ })
-
begin
-
version = context[:version]
-
project = version.project
-
-
return '' unless Backlogs.configured?(project)
-
-
snippet = ''
-
-
if User.current.allowed_to?(:edit_wiki_pages, project)
-
snippet += '<span id="edit_wiki_page_action">'
-
snippet += link_to l(:button_edit_wiki), {:controller => 'rb_wikis', :action => 'edit', :project_id => project.id, :sprint_id => version.id }, :class => 'icon icon-edit'
-
snippet += '</span>'
-
-
# this wouldn't be necesary if the schedules plugin
-
# didn't disable the contextual hook
-
snippet += javascript_include_tag 'jquery/jquery-1.6.2.min.js', :plugin => 'redmine_backlogs'
-
snippet += <<-generatedscript
-
-
<script type="text/javascript">
-
var $j = jQuery.noConflict();
-
$j(document).ready(function() {
-
$j('#edit_wiki_page_action').detach().appendTo("div.contextual");
-
});
-
</script>
-
generatedscript
-
end
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def view_my_account(context={ })
-
begin
-
return %{
-
<h3>#{l(:label_backlogs)}</h3>
-
<div class="box tabular">
-
<p>
-
#{label :backlogs, :task_color}
-
#{text_field :backlogs, :task_color, :value => context[:user].backlogs_preference[:task_color]}
-
</p>
-
</div>
-
}
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
end
-
-
1
def controller_issues_new_after_save(context={ })
-
2
params = context[:params]
-
2
issue = context[:issue]
-
-
2
return unless Backlogs.configured?(issue.project)
-
-
2
if issue.is_story?
-
2
if params[:link_to_original]
-
2
rel = IssueRelation.new
-
-
2
rel.issue_from_id = Integer(params[:link_to_original])
-
2
rel.issue_to_id = issue.id
-
2
rel.relation_type = IssueRelation::TYPE_RELATES
-
2
rel.save
-
end
-
-
2
if params[:copy_tasks] =~ /^[a-z]+:[0-9]+$/
-
1
action, id = *(params[:copy_tasks].split(/:/))
-
-
1
story = RbStory.find(Integer(id))
-
-
1
if action != 'none'
-
1
case action
-
when 'open'
-
3
tasks = story.tasks.select{|t| !t.reload.closed?}
-
when 'none'
-
tasks = []
-
when 'all'
-
tasks = story.tasks
-
else
-
raise "Unexpected value #{params[:copy_tasks]}"
-
end
-
-
1
tasks.each {|t|
-
2
nt = Issue.new
-
2
nt.copy_from(t)
-
2
nt.parent_issue_id = issue.id
-
2
nt.position = nil # will assign a new position
-
2
nt.save!
-
}
-
end
-
end
-
end
-
end
-
-
1
def controller_issues_edit_after_save(context={ })
-
params = context[:params]
-
issue = context[:issue]
-
-
if issue.is_task?
-
begin
-
issue.remaining_hours = Float(params[:remaining_hours])
-
rescue ArgumentError, TypeError
-
issue.remaining_hours = nil
-
end
-
issue.save
-
end
-
end
-
-
1
def view_layouts_base_html_head(context={})
-
466
return '' if Setting.login_required? && !User.current.logged?
-
-
466
if User.current.admin? && !context[:request].session[:backlogs_configured]
-
context[:request].session[:backlogs] = Backlogs.configured?
-
context[:controller].send(:flash)[:error] = l(:label_backlogs_unconfigured) if !context[:request].session[:backlogs]
-
end
-
-
466
return context[:controller].send(:render_to_string, {:locals => context}.merge(:partial=> 'hooks/rb_include_scripts'))
-
end
-
-
1
def view_timelog_edit_form_bottom(context={ })
-
2
time_entry = context[:time_entry]
-
2
return '' if time_entry[:issue_id].blank?
-
-
1
issue = Issue.find(context[:time_entry].issue_id)
-
return '' unless Backlogs.configured?(issue.project) &&
-
1
Backlogs.setting[:timelog_from_taskboard]=='enabled'
-
1
snippet=''
-
-
1
begin
-
1
if issue.is_task? && User.current.allowed_to?(:update_remaining_hours, time_entry.project) != nil
-
1
remaining_hours = issue.remaining_hours
-
1
snippet += "<p><label for='remaining_hours'>#{l(:field_remaining_hours)}</label>"
-
1
snippet += text_field_tag('remaining_hours', remaining_hours, :size => 6)
-
1
snippet += '</p>'
-
end
-
1
return snippet
-
rescue => e
-
exception(context, e)
-
return ''
-
end
-
-
end
-
-
1
def controller_timelog_edit_before_save(context={ })
-
2
time_entry = context[:time_entry]
-
2
return '' if time_entry[:issue_id].blank?
-
-
1
params = context[:params]
-
1
return unless params.include?("commit")
-
-
1
issue = Issue.find(time_entry.issue_id)
-
return unless Backlogs.configured?(issue.project) &&
-
1
Backlogs.setting[:timelog_from_taskboard]=='enabled'
-
-
1
if issue.is_task? && User.current.allowed_to?(:update_remaining_hours, time_entry.project) != nil
-
1
remaining_hours = params[:remaining_hours].gsub(',','.').to_f
-
1
if remaining_hours != issue.remaining_hours
-
1
issue.journalized_update_attribute(:remaining_hours, remaining_hours) if time_entry.save
-
end
-
end
-
end
-
-
end
-
end
-
end
-
1
require_dependency 'issue'
-
-
1
module Backlogs
-
1
module IssuePatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable
-
-
1
acts_as_list_with_gaps :default => (Backlogs.setting[:new_story_position] == 'bottom' ? 'bottom' : 'top')
-
-
1
safe_attributes 'position'
-
1
before_save :backlogs_before_save
-
1
after_save :backlogs_after_save
-
-
1
include Backlogs::ActiveRecord::Attributes
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def is_story?
-
2772
return RbStory.trackers.include?(tracker_id)
-
end
-
-
1
def is_task?
-
2728
return (tracker_id == RbTask.tracker)
-
end
-
-
1
def story
-
3482
if @rb_story.nil?
-
2760
if self.new_record?
-
1040
parent_id = self.parent_id
-
1040
parent_id = self.parent_issue_id if parent_id.blank?
-
1040
parent_id = nil if parent_id.blank?
-
1040
parent = parent_id ? Issue.find(parent_id) : nil
-
-
1040
if parent.nil?
-
918
@rb_story = nil
-
elsif parent.is_story?
-
122
@rb_story = parent.becomes(RbStory)
-
else
-
@rb_story = parent.story
-
end
-
else
-
1720
@rb_story = Issue.find(:first, :order => 'lft DESC', :conditions => [ "root_id = ? and lft < ? and rgt > ? and tracker_id in (?)", root_id, lft, rgt, RbStory.trackers ])
-
1720
@rb_story = @rb_story.becomes(RbStory) if @rb_story
-
end
-
end
-
3482
return @rb_story
-
end
-
-
1
def blocks
-
# return issues that I block that aren't closed
-
109
return [] if closed?
-
109
begin
-
175
return relations_from.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_to.closed? ? ir.issue_to : nil }.compact
-
rescue
-
# stupid rails and their ignorance of proper relational databases
-
Rails.logger.error "Cannot return the blocks list for #{self.id}: #{e}"
-
return []
-
end
-
end
-
-
1
def blockers
-
# return issues that block me
-
return [] if closed?
-
relations_to.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed? ? ir.issue_from : nil}.compact
-
end
-
-
1
def velocity_based_estimate
-
2
return nil if !self.is_story? || ! self.story_points || self.story_points <= 0
-
-
hpp = self.project.scrum_statistics.hours_per_point
-
return nil if ! hpp
-
-
return Integer(self.story_points * (hpp / 8))
-
end
-
-
1
def backlogs_before_save
-
1483
if Backlogs.configured?(project) && (self.is_task? || self.story)
-
400
self.remaining_hours = self.estimated_hours if self.remaining_hours.blank?
-
400
self.estimated_hours = self.remaining_hours if self.estimated_hours.blank?
-
-
400
self.remaining_hours = 0 if self.status.backlog_is?(:success)
-
-
400
self.fixed_version = self.story.fixed_version if self.story
-
400
self.start_date = Date.today if self.start_date.blank? && self.status_id != IssueStatus.default.id
-
-
400
self.tracker = Tracker.find(RbTask.tracker) unless self.tracker_id == RbTask.tracker
-
elsif self.is_story?
-
1083
self.remaining_hours = self.leaves.sum("COALESCE(remaining_hours, 0)").to_f
-
1083
if self.fixed_version
-
689
self.start_date ||= (self.fixed_version.sprint_start_date || Date.today)
-
689
self.due_date = self.fixed_version.effective_date || Date.today
-
689
self.due_date = self.start_date if self.due_date < self.start_date if self.due_date
-
else
-
394
self.start_date = nil
-
394
self.due_date = nil
-
end
-
end
-
-
1483
self.move_to_top if self.position.blank? || (@copied_from.present? && @copied_from.position == self.position)
-
-
# scrub position from the journal by copying the new value to the old
-
1483
@attributes_before_change['position'] = self.position if @attributes_before_change
-
-
1483
@backlogs_new_record = self.new_record?
-
-
1483
return true
-
end
-
-
1
def backlogs_after_save
-
1483
RbJournal.rebuild(self) if @backlogs_new_record
-
-
1483
return unless Backlogs.configured?(self.project)
-
-
1483
if self.is_story?
-
# raw sql and manual journal here because not
-
# doing so causes an update loop when Issue calls
-
# update_parent :<
-
1083
tasks_updated = []
-
1083
Issue.find(:all, :conditions => ["root_id=? and lft>? and rgt<? and
-
(
-
(? is NULL and not fixed_version_id is NULL)
-
or
-
(not ? is NULL and fixed_version_id is NULL)
-
or
-
(not ? is NULL and not fixed_version_id is NULL and ?<>fixed_version_id)
-
)", self.root_id, self.lft, self.rgt,
-
self.fixed_version_id, self.fixed_version_id,
-
self.fixed_version_id, self.fixed_version_id]).each{|task|
-
8
case Backlogs.platform
-
when :redmine
-
8
j = Journal.new
-
8
j.journalized = task
-
8
j.created_on = self.updated_on
-
8
j.details << JournalDetail.new(:property => 'attr', :prop_key => 'fixed_version_id', :old_value => task.fixed_version_id, :value => fixed_version_id)
-
when :chiliproject
-
j = IssueJournal.new
-
j.created_at = self.updated_on
-
j.details['fixed_version_id'] = [task.fixed_version_id, self.fixed_version_id]
-
j.activity_type = 'issues'
-
j.journaled = task
-
j.version = task.last_journal.version + 1
-
end
-
8
j.user = User.current
-
8
j.save!
-
-
8
tasks_updated << task
-
}
-
-
1083
if tasks_updated.size > 0
-
16
tasklist = '(' + tasks_updated.collect{|task| connection.quote(task.id)}.join(',') + ')'
-
8
connection.execute("update issues set
-
updated_on = #{connection.quote(self.updated_on)}, fixed_version_id = #{connection.quote(self.fixed_version_id)}
-
where id in #{tasklist}")
-
end
-
-
1083
connection.execute("update issues
-
set tracker_id = #{RbTask.tracker}
-
where root_id = #{self.root_id} and lft > #{self.lft} and rgt < #{self.rgt}")
-
end
-
-
1483
if self.story || self.is_task?
-
400
connection.execute("update issues set tracker_id = #{RbTask.tracker} where root_id = #{self.root_id} and lft >= #{self.lft} and rgt <= #{self.rgt}")
-
end
-
end
-
-
1
def value_at(property, time)
-
return history(property, [time.to_date])[0]
-
end
-
-
1
def history(property, days)
-
280
property = property.to_s unless property.is_a?(String)
-
280
raise "Unsupported property #{property.inspect}" unless RbJournal::JOURNALED_PROPERTIES.include?(property)
-
-
280
days = days.to_a
-
280
created_day = created_on.to_date
-
3086
active_days = days.select{|d| d >= created_day}
-
-
# if not active, don't do anything
-
280
return [nil] * (days.size + 1) if active_days.size == 0
-
-
# anything before the creation date is nil
-
232
prefix = [nil] * (days.size - active_days.size)
-
-
# add one extra day as start-of-first-day
-
232
active_days.unshift(active_days[0] - 1)
-
-
232
journal = RbJournal.find(:all, :conditions => ['issue_id = ? and property = ?', self.id, property], :order => :timestamp).to_a
-
232
if journal.size == 0
-
RbJournal.rebuild(self)
-
journal = RbJournal.find(:all, :conditions => ['issue_id = ? and property = ?', self.id, property], :order => :timestamp).to_a
-
raise "Journal cannot have 0 entries" if journal.size == 0
-
end
-
-
232
values = [journal[0].value] * active_days.size
-
-
232
journal.each{|change|
-
309
stamp = change.timestamp.to_date
-
844
day = active_days.index{|d| d >= stamp}
-
309
break if day.nil?
-
309
values.fill(change.value, day)
-
}
-
-
# ignore the start-of-day value for issues created mid-sprint
-
232
values[0] = nil if created_day > days[0]
-
-
232
return prefix + values
-
end
-
-
end
-
end
-
end
-
-
1
Issue.send(:include, Backlogs::IssuePatch) unless Issue.included_modules.include? Backlogs::IssuePatch
-
1
require_dependency 'user'
-
-
1
module Backlogs
-
1
module IssueStatusPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def backlog
-
1451
return :success if is_closed? && (default_done_ratio.nil? || default_done_ratio == 100)
-
1392
return :failure if is_closed?
-
1392
return :new if is_default? || default_done_ratio == 0
-
96
return :in_progress
-
end
-
-
1
def backlog_is?(states)
-
1451
states = [states] unless states.is_a?(Array)
-
1451
raise "Not a valid state set #{states.inspect}" unless (states - [:success, :failure, :new, :in_progress]) == []
-
1451
return states.include?(backlog)
-
end
-
end
-
end
-
end
-
-
1
IssueStatus.send(:include, Backlogs::IssueStatusPatch) unless IssueStatus.included_modules.include? Backlogs::IssueStatusPatch
-
1
require_dependency 'issues_controller'
-
1
require 'rubygems'
-
1
require 'nokogiri'
-
1
require 'json'
-
-
1
module Backlogs
-
1
module IssuesControllerPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable # Send unloadable so it will not be unloaded in development
-
1
after_filter :add_backlogs_fields, :only => [:index]
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def add_backlogs_fields
-
14
story_trackers = RbStory.trackers
-
-
14
case params[:format]
-
when 'xml'
-
body = Nokogiri::XML(response.body)
-
body.xpath('//issue').each{|issue|
-
next unless story_trackers.include?(Integer(issue.at('.//tracker')['id']))
-
issue << body.create_element('story_points', RbStory.find(issue.at('.//id').text).story_points.to_s)
-
}
-
response.body = body.to_xml
-
when 'json'
-
body = JSON.parse(response.body)
-
body['issues'].each{|issue|
-
next unless story_trackers.include?(issue['tracker']['id'])
-
issue['story_points'] = RbStory.find(issue['id']).story_points
-
}
-
response.body = body.to_json
-
end
-
end
-
end
-
end
-
end
-
-
1
IssuesController.send(:include, Backlogs::IssuesControllerPatch) unless IssuesController.included_modules.include? Backlogs::IssuesControllerPatch
-
1
require_dependency 'journal'
-
-
1
module Backlogs
-
1
module JournalPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable
-
-
1
after_save :backlogs_after_save
-
-
# added because acts_as_journal acts wonky -- some properties only show up after the 2nd save
-
1
attr_accessor :rb_journal_properties_saved
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def backlogs_after_save
-
119
RbJournal.journal(self)
-
end
-
end
-
end
-
end
-
-
1
Journal.send(:include, Backlogs::JournalPatch) unless Journal.included_modules.include? Backlogs::JournalPatch
-
1
module Backlogs
-
1
class MergedArray
-
1
class FlexObject < Hash
-
1
def initialize(data = {})
-
1328
super
-
-
1328
data.each_pair {|k, v|
-
1328
raise "#{k} is not a symbol" unless k.is_a?(Symbol)
-
1328
self[k] = v
-
}
-
end
-
-
1
def [](key)
-
15585
raise "Key '#{key}' does not exist" unless self.include?(key)
-
15585
raise "Key '#{key}' is not a symbol" unless key.is_a?(Symbol)
-
15585
return super(key)
-
end
-
-
1
def []=(key, value)
-
10705
raise "Key '#{key}' is not a symbol" unless key.is_a?(Symbol)
-
10705
super(key, value)
-
end
-
-
1
def method_missing(method_sym, *arguments, &block)
-
13556
if method_sym.to_s =~ /=$/ && arguments.size == 1
-
1156
self[method_sym.to_s.gsub(/=$/, '').intern] = arguments[0]
-
12400
elsif self.include?(method_sym)
-
12400
return self[method_sym]
-
else
-
super(method_sym, *arguments, &block)
-
end
-
end
-
-
1
def nilify
-
1818
keys.each{|k| self[k] = nil}
-
end
-
end
-
-
1
def initialize(arrays = {})
-
104
@data = nil
-
104
merge(arrays)
-
end
-
-
1
def merge(arrays)
-
580
arrays.each_pair do |name, data|
-
476
raise "#{name} is not a symbol" unless name.is_a?(Symbol)
-
476
raise "#{name} is not a array" unless data.is_a?(Array)
-
-
476
if @data
-
372
raise "#{name} must have length of #{@data.size}, actual size #{data.size}" unless @data == [] || data.size == @data.size
-
5880
@data.zip(data).each {|cell, v| cell[name] = v }
-
else
-
1432
@data = data.collect{|v| FlexObject.new(name => v) }
-
end
-
end
-
end
-
-
1
def add(arrays)
-
91
return unless arrays
-
91
arrays.each_pair do |name, data|
-
268
next if data.nil?
-
268
raise "#{name} is not a symbol" unless name.is_a?(Symbol)
-
268
raise "#{name} is not a array" unless data.is_a?(Array)
-
268
raise "#{name} not initialized" unless @data && @data.size > 0 && @data[0].include?(name)
-
268
raise "data series '#{name}' is too long (got #{data.size}, maximum accepted #{@data.size})" if data.size > @data.size
-
-
268
data.each_with_index{|d, i|
-
2990
@data[i][name] += d if d
-
}
-
end
-
end
-
-
1
def [](i)
-
return @data[i]
-
end
-
-
1
def each(&block)
-
621
@data.each {|cell| block.call(cell) }
-
end
-
1
def each_with_index(&block)
-
294
@data.each_with_index {|cell, index| block.call(cell, index) }
-
end
-
-
1
def collect(&block)
-
5791
@data.collect {|cell| block.call(cell) }
-
end
-
-
1
def series(name)
-
2157
@data.collect{|cell| cell[name]}
-
end
-
-
1
def to_s
-
return @data.to_s
-
end
-
1
def inspect
-
return @data.collect{|d| d.inspect}.join("\n")
-
end
-
end
-
end
-
1
require_dependency 'my_controller'
-
-
1
module Backlogs
-
1
module MyControllerPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable # Send unloadable so it will not be unloaded in development
-
1
after_filter :save_backlogs_preferences, :only => [:account]
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def save_backlogs_preferences
-
if request.post? && flash[:notice] == l(:notice_account_updated)
-
color = (params[:backlogs] ? params[:backlogs][:task_color] : '').to_s
-
if color == '' || color.match(/^#[A-Fa-f0-9]{6}$/)
-
User.current.backlogs_preference[:task_color] = color
-
else
-
flash[:notice] = "Invalid task color code #{color}"
-
end
-
end
-
end
-
end
-
end
-
end
-
-
1
MyController.send(:include, Backlogs::MyControllerPatch) unless MyController.included_modules.include? Backlogs::MyControllerPatch
-
1
require 'rubygems'
-
1
require 'prawn'
-
1
require 'prawn/measurement_extensions'
-
1
require 'net/http'
-
-
1
require 'yaml'
-
1
require 'uri/common'
-
1
require 'open-uri/cached'
-
1
require 'zlib'
-
1
require 'nokogiri'
-
-
1
unless defined?('ReliableTimout') || defined?(:ReliableTimout)
-
if Backlogs.gems.include?('system_timer')
-
require 'system_timer'
-
ReliableTimout = SystemTimer
-
else
-
require 'timeout'
-
ReliableTimout = Timeout
-
end
-
end
-
-
1
class String
-
1
def units_to_points
-
1317
return Float(self) if self =~/[0-9]$/
-
-
1065
m = self.match(/(.*)(mm|pt|in)$/)
-
1065
if m
-
1065
value = m[1].strip
-
1065
units = m[2]
-
else
-
value = self
-
units = nil
-
end
-
1065
raise "No units found for #{self.inspect}" unless m
-
-
1065
value = Float(value.gsub(/\.$/, ''))
-
1065
case units
-
when nil
-
return value
-
-
when 'mm'
-
637
return value * 2.8346457
-
-
when 'pt'
-
102
return value
-
-
when 'in'
-
326
return value * 72
-
-
else
-
raise "Unexpected unit specification for #{self}"
-
end
-
end
-
end
-
-
1
module BacklogsPrintableCards
-
1
class CardPageLayout
-
1
def initialize(layout)
-
176
@layout = layout
-
-
176
begin
-
176
@top_margin = layout['top_margin'].units_to_points
-
176
@height = layout['height'].units_to_points
-
176
@vertical_pitch = layout['vertical_pitch'].units_to_points
-
176
@vertical_pitch = @height if @vertical_pitch == 0
-
-
176
@left_margin = layout['left_margin'].units_to_points
-
176
@width = layout['width'].units_to_points
-
176
@horizontal_pitch = layout['horizontal_pitch'].units_to_points
-
176
@horizontal_pitch = @width if @horizontal_pitch == 0
-
-
176
@across = Integer(layout['across'])
-
176
@down = Integer(layout['down'])
-
-
176
@papersize = layout['papersize'].upcase
-
176
@name = layout['name']
-
176
@source = layout['source']
-
-
176
geom = Prawn::Document::PageGeometry::SIZES[@papersize]
-
176
if geom.nil?
-
Rails.logger.error "Backlogs printable cards: paper size '#{@papersize}' for label #{@name} not supported"
-
@valid = false
-
return
-
end
-
-
176
@paper_width = geom[0]
-
176
@paper_height = geom[1]
-
176
@paper_size = layout['papersize']
-
-
176
@valid = false
-
176
if @down < 1
-
Rails.logger.error "Backlogs printable cards: #{@name} has no rows"
-
176
elsif @across < 1
-
Rails.logger.error "Backlogs printable cards: #{@name} has no columns"
-
176
elsif @height > @vertical_pitch
-
Rails.logger.error "Backlogs printable cards: #{@name} card height exceeds vertical pitch"
-
176
elsif @width > @horizontal_pitch
-
Rails.logger.error "Backlogs printable cards: #{@name} card width exceeds horizontal pitch"
-
else
-
176
@valid = true
-
end
-
rescue => e
-
Rails.logger.error "Backlogs printable cards: error loading #{layout['name']}: #{e}"
-
@valid = false
-
end
-
end
-
-
1
attr_reader :left_margin, :horizontal_pitch, :width
-
1
attr_reader :top_margin, :vertical_pitch, :height
-
1
attr_reader :across, :down
-
1
attr_reader :paper_width, :paper_height, :paper_size
-
1
attr_reader :source
-
1
attr_reader :valid
-
-
1
def self.selected
-
55
return @@layouts[Backlogs.setting[:card_spec]]
-
end
-
-
1
def self.available
-
return @@layouts.keys.sort
-
end
-
-
1
def to_yaml(opts={})
-
return @layout.reject{|k, v| k == 'name'}.to_yaml(opts)
-
end
-
-
1
def self.update
-
# clean up existing labels
-
malformed_labels = {}
-
-
['avery-iso-templates.xml', 'avery-other-templates.xml', 'avery-us-templates.xml', 'brother-other-templates.xml', 'dymo-other-templates.xml',
-
'maco-us-templates.xml', 'misc-iso-templates.xml', 'misc-other-templates.xml', 'misc-us-templates.xml', 'pearl-iso-templates.xml',
-
'uline-us-templates.xml', 'worldlabel-us-templates.xml', 'zweckform-iso-templates.xml'].each {|filename|
-
-
uri = URI.parse("http://git.gnome.org/browse/glabels/plain/templates/#{filename}")
-
labels = nil
-
-
if ! ENV['http_proxy'].blank?
-
begin
-
proxy = URI.parse(ENV['http_proxy'])
-
if proxy.userinfo
-
user, pass = proxy.userinfo.split(/:/)
-
else
-
user = pass = nil
-
end
-
labels = Net::HTTP::Proxy(proxy.host, proxy.port, user, pass).start(uri.host) {|http| http.get(uri.path)}.body
-
rescue URI::Error => e
-
puts "Setup proxy failed: #{e}"
-
labels = nil
-
end
-
end
-
-
begin
-
labels = Net::HTTP.get_response(uri).body if labels.nil?
-
rescue
-
labels = nil
-
end
-
-
if labels.nil?
-
puts "Could not fetch #{filename}"
-
next
-
end
-
-
doc = Nokogiri::XML(labels)
-
-
doc.xpath('Glabels-templates/Template').each { |specs|
-
label = nil
-
-
papersize = specs['size']
-
papersize = 'Letter' if papersize == 'US-Letter'
-
-
specs.xpath('Label-rectangle').each { |geom|
-
margin = nil
-
geom.xpath('Markup-margin').each { |m| margin = m['size'] }
-
margin = "1mm" if margin.blank?
-
-
geom.xpath('Layout').each { |layout|
-
label = {
-
'inner_margin' => margin,
-
'across' => Integer(layout['nx']),
-
'down' => Integer(layout['ny']),
-
'top_margin' => layout['y0'],
-
'height' => geom['height'],
-
'horizontal_pitch' => layout['dx'],
-
'left_margin' => layout['x0'],
-
'width' => geom['width'],
-
'vertical_pitch' => layout['dy'],
-
'papersize' => papersize,
-
'source' => 'glabel'
-
}
-
}
-
}
-
-
next if label.nil?
-
-
key = "#{specs['brand']} #{specs['part']}"
-
label['name'] = key
-
-
stock = CardPageLayout.new(label)
-
if !stock.valid
-
puts "Skipping malformed label '#{key}' from #{filename}"
-
malformed_labels[key] = stock.to_yaml
-
else
-
@@layouts[key] = stock if not @@layouts[key] or @@layouts[key].source == 'glabel'
-
-
specs.xpath('Alias').each { |also|
-
aliaskey = "#{also['brand']} #{also['part']}"
-
@@layouts[aliaskey] = stock if not @@layouts[aliaskey] or @@layouts[aliaskey].source == 'glabel'
-
}
-
end
-
}
-
}
-
-
File.open(File.dirname(__FILE__) + '/labels.yaml', 'w') do |dump|
-
YAML.dump(@@layouts, dump)
-
end
-
File.open(File.dirname(__FILE__) + '/labels-malformed.yaml', 'w') do |dump|
-
YAML.dump(malformed_labels, dump)
-
end
-
end
-
-
1
@@layouts ||= {}
-
1
begin
-
1
layouts = YAML::load_file(File.dirname(__FILE__) + '/labels.yaml')
-
1
layouts.each_pair{|key, spec|
-
176
layout = CardPageLayout.new(spec.merge({'name' => key}))
-
176
@@layouts[key] = layout if layout.valid
-
}
-
rescue => e
-
Rails.logger.error "Backlogs printable cards: problem loading labels: #{e}"
-
end
-
end
-
-
# put the mixins in a separate class, seems to interfere with prawn otherwise
-
1
class Gravatar
-
1
include GravatarHelper::PublicMethods
-
1
include ERB::Util
-
-
1
def initialize(email, size)
-
# see conversion chart pt -> px @ http://sureshjain.wordpress.com/2007/07/06/53/
-
2
@url = gravatar_url(email, :size => (size * 16) / 12)
-
end
-
-
1
def image
-
2
begin
-
2
ReliableTimout.timeout(10) do
-
return open(@url)
-
end
-
rescue
-
2
return nil
-
end
-
end
-
-
1
attr_reader :url
-
end
-
-
1
class CardTemplate
-
1
def initialize(width, height, template)
-
4
@gravatar_online = true
-
-
4
f = nil
-
4
['-default', ''].each {|postfix|
-
8
t = File.dirname(__FILE__) + "/#{template}#{postfix}.glabels"
-
8
f = t if File.exists?(t)
-
}
-
4
raise "No template for #{template}" unless f
-
4
label = Nokogiri::XML(Zlib::GzipReader.open(f))
-
-
4
bounds = label.xpath('//ns:Template/ns:Label-rectangle', 'ns' => 'http://snaught.com/glabels/2.2/')[0]
-
4
@template = { :x => bounds['width'].units_to_points, :y => bounds['height'].units_to_points}
-
-
4
@card = label.xpath('//ns:Objects', 'ns' => 'http://snaught.com/glabels/2.2/')[0]
-
4
@width = width
-
4
@height = height
-
end
-
-
1
def box(b, scaled=true)
-
return {
-
51
:x => (b['x'].units_to_points / @template[:x]) * @width,
-
51
:y => (1 - (b['y'].units_to_points / @template[:y])) * @height,
-
51
:w => (b['w'].units_to_points / @template[:x]) * @width,
-
51
:h => (b['h'].units_to_points / @template[:y]) * @height
-
51
}
-
end
-
-
1
def style(b)
-
49
s = b.xpath('ns:Span', 'ns' => 'http://snaught.com/glabels/2.2/')[0]
-
49
style = [s['font_weight'] == "Bold" ? 'bold' : nil, s['font_italic'] == "True" ? 'italic' : nil].compact.join('_')
-
49
style = 'normal' if style == ''
-
return {
-
:size => Integer(s['font_size']),
-
:style => style.intern
-
49
}
-
end
-
-
1
def line_width(obj)
-
7
return obj['line_width'].units_to_points
-
end
-
-
1
def color(obj, prop)
-
56
c = obj[prop]
-
56
return nil if c =~ /00$/
-
56
raise "Alpha channel not supported" unless c =~ /ff$/i
-
56
return c[2, 6]
-
end
-
-
1
def line(l)
-
return {
-
7
:x1 => (l['x'].units_to_points / @template[:x]) * @width,
-
7
:y1 => (1 - (l['y'].units_to_points / @template[:y])) * @height,
-
7
:x2 => ((l['x'].units_to_points + l['dx'].units_to_points) / @template[:x]) * @width,
-
7
:y2 => (1 - ((l['y'].units_to_points + l['dy'].units_to_points) / @template[:y])) * @height
-
7
}
-
end
-
-
1
def render(x, y, pdf, data)
-
7
default_stroke_color = pdf.stroke_color
-
7
default_fill_color = pdf.fill_color
-
-
7
pdf.bounding_box [x, y], :width => @width, :height => @height do
-
7
@card.children.each {|obj|
-
133
next if obj.text?
-
-
63
case obj.name
-
when 'Object-box'
-
dim = box(obj)
-
pdf.fill_color = color(obj, 'fill_color') || default_fill_color
-
pdf.stroke_color = color(obj, 'line_color') || default_stroke_color
-
pdf.line_width = line_width(obj)
-
-
pdf.stroke {
-
if color(obj, 'fill_color')
-
pdf.fill_rectangle [312,260], 180, 16
-
else
-
pdf.rectangle [dim[:x], dim[:y]], dim[:w], dim[:h]
-
end
-
}
-
-
-
when 'Object-line'
-
7
dim = line(obj)
-
7
pdf.line_width = line_width(obj)
-
7
pdf.stroke_color = color(obj, 'line_color') || default_stroke_color
-
-
7
pdf.stroke {
-
7
pdf.line([dim[:x1], dim[:y1]], [dim[:x2], dim[:y2]])
-
}
-
-
when 'Object-text'
-
49
dim = box(obj)
-
-
49
pdf.fill_color = color(obj.xpath('ns:Span', 'ns' => 'http://snaught.com/glabels/2.2/')[0], 'color') || default_fill_color
-
-
49
content = ''
-
49
obj.xpath('ns:Span', 'ns' => 'http://snaught.com/glabels/2.2/')[0].children.each {|t|
-
147
if t.text?
-
98
content << t.text
-
elsif t.name == 'Field'
-
49
f = data[t['name']]
-
49
raise "Unsupported card variable '#{t['name']}" unless f
-
49
content << f
-
else
-
raise "Unsupported text object '#{t.name}'"
-
end
-
}
-
-
49
content.strip!
-
-
49
s = style(obj)
-
49
pdf.font_size(s[:size]) do
-
49
Prawn::Text::Box.new(content, {:overflow => :ellipses, :at => [dim[:x], dim[:y]], :document => pdf, :width => dim[:w], :height => dim[:h], :style => s[:style]}).render
-
end
-
-
when 'Object-image'
-
7
if data['owner.email'] && @gravatar_online
-
2
dim = box(obj)
-
-
2
img = Gravatar.new(data['owner.email'], (dim[:h] < dim[:w]) ? dim[:h] : dim[:w]).image
-
2
if img
-
pdf.image img, :at => [dim[:x], dim[:y]], :width => dim[:w]
-
else
-
# if image loading fails once, stop loading images for this rendering
-
2
@gravatar_online = false
-
end
-
end
-
-
else
-
raise "Unsupported object '#{obj.name}'"
-
end
-
}
-
end
-
-
7
pdf.stroke_color = default_stroke_color
-
7
pdf.fill_color = default_fill_color
-
end
-
end
-
-
1
class PrintableCards
-
1
include Redmine::I18n
-
-
1
def initialize(stories, with_tasks, lang)
-
2
set_language_if_valid lang
-
-
2
@label = CardPageLayout.selected
-
2
@pdf = Prawn::Document.new(
-
:page_layout => :portrait,
-
:left_margin => 0,
-
:right_margin => 0,
-
:top_margin => 0,
-
:bottom_margin => 0,
-
:page_size => @label ? @label.paper_size : 'A4')
-
-
2
if !@label
-
@pdf.text("No (valid) label layout was selected. Your rails log will probably have more details on the exact problem.")
-
else
-
2
@story = CardTemplate.new(@label.width, @label.height, 'story')
-
2
@task = CardTemplate.new(@label.width, @label.height, 'task')
-
-
2
fontdir = File.dirname(__FILE__) + '/ttf'
-
2
@pdf.font_families.update(
-
"DejaVuSans" => {
-
:bold => "#{fontdir}/DejaVuSans-Bold.ttf",
-
:italic => "#{fontdir}/DejaVuSans-Oblique.ttf",
-
:bold_italic => "#{fontdir}/DejaVuSans-BoldOblique.ttf",
-
:normal => "#{fontdir}/DejaVuSans.ttf"
-
}
-
)
-
2
@pdf.font "DejaVuSans"
-
-
2
@cards = 0
-
-
2
case Backlogs.setting[:taskboard_card_order]
-
when 'tasks_follow_story'
-
stories.each { |story|
-
add(story)
-
-
if with_tasks
-
story.descendants.each {|task|
-
add(task)
-
}
-
end
-
}
-
-
when 'stories_then_tasks'
-
stories.each { |story|
-
add(story)
-
}
-
-
if with_tasks
-
@cards = 0
-
@pdf.start_new_page
-
-
stories.each { |story|
-
story.descendants.each {|task|
-
add(task)
-
}
-
}
-
end
-
-
else # 'story_follows_tasks'
-
2
stories.each { |story|
-
7
if with_tasks
-
3
story.descendants.each {|task|
-
add(task)
-
}
-
end
-
-
7
add(story)
-
}
-
end
-
end
-
end
-
-
1
attr_reader :pdf
-
-
1
def add(issue)
-
7
row = @cards % @label.down
-
7
col = Integer(@cards / @label.down) % @label.across
-
7
@cards += 1
-
-
7
@pdf.start_new_page if row == 0 and col == 0 and @cards != 1
-
-
7
x = @label.left_margin + (@label.horizontal_pitch * col)
-
7
y = @label.paper_height - (@label.top_margin + (@label.vertical_pitch * row))
-
-
7
data = {}
-
7
if issue.is_task?
-
data['story.position'] = issue.story.position ? issue.story.position : l(:label_not_prioritized)
-
data['story.id'] = issue.story.id
-
data['story.subject'] = issue.story.subject
-
-
data['id'] = issue.id
-
data['subject'] = issue.subject.to_s.strip
-
data['description'] = issue.description.to_s.strip; data['description'] = data['subject'] if data['description'] == ''
-
data['category'] = issue.category ? issue.category.name : ''
-
data['hours.estimated'] = (issue.estimated_hours || '?').to_s + ' ' + l(:label_hours)
-
data['position'] = issue.position ? issue.position : l(:label_not_prioritized)
-
data['path'] = (issue.self_and_ancestors.reverse.collect{|i| "#{i.tracker.name} ##{i.id}"}.join(" : ")) + " (#{data['story.position']})"
-
data['sprint.name'] = issue.fixed_version ? issue.fixed_version.name : I18n.t(:backlogs_product_backlog)
-
data['owner'] = issue.assigned_to.blank? ? "" : "#{issue.assigned_to.name}"
-
data['owner.email'] = issue.assigned_to.blank? ? nil : issue.assigned_to.mail.to_s.downcase
-
-
card = @task
-
-
elsif issue.is_story?
-
7
data['id'] = issue.id
-
7
data['subject'] = issue.subject
-
7
data['description'] = issue.description.to_s.strip; data['description'] = data['subject'] if data['description'] == ''
-
7
data['category'] = issue.category ? issue.category.name : ''
-
7
data['size'] = (issue.story_points ? "#{issue.story_points}" : '?') + ' ' + l(:label_points)
-
7
data['position'] = issue.position ? issue.position : l(:label_not_prioritized)
-
14
data['path'] = (issue.self_and_ancestors.reverse.collect{|i| "#{i.tracker.name} ##{i.id}"}.join(" : ")) + " (#{data['position']})"
-
7
data['sprint.name'] = issue.fixed_version ? issue.fixed_version.name : I18n.t(:backlogs_product_backlog)
-
7
data['owner'] = issue.assigned_to.blank? ? "" : "#{issue.assigned_to.name}"
-
7
data['owner.email'] = issue.assigned_to.blank? ? nil : issue.assigned_to.mail.to_s.downcase
-
-
7
card = @story
-
-
else
-
raise "Unsupported card type '#{type}'"
-
-
end
-
-
77
data.keys.each {|d| data[d] = data[d].to_s }
-
-
7
card.render(x, y, @pdf, data)
-
end
-
-
end
-
end
-
1
require_dependency 'project'
-
-
1
module Backlogs
-
1
class Statistics
-
1
def initialize(project)
-
1
@project = project
-
1
@statistics = {:succeeded => [], :failed => [], :values => {}}
-
-
1
@active_sprint = RbSprint.find(:first, :conditions => ["project_id = ? and status = 'open' and not (sprint_start_date is null or effective_date is null) and ? between sprint_start_date and effective_date", @project.id, Date.today])
-
1
@past_sprints = RbSprint.find(:all,
-
:conditions => ["project_id = ? and not(effective_date is null or sprint_start_date is null) and effective_date < ?", @project.id, Date.today],
-
:order => "effective_date desc",
-
:limit => 5).select(&:has_burndown?)
-
-
9
@points_per_day = @past_sprints.collect{|s| s.burndown('up')[:points_committed][0]}.compact.sum / @past_sprints.collect{|s| s.days(:all).size}.compact.sum if @past_sprints.size > 0
-
-
1
@all_sprints = (@past_sprints + [@active_sprint]).compact
-
-
1
if @all_sprints.size != 0
-
5
@velocity = @past_sprints.collect{|sprint| sprint.burndown('up')[:points_accepted][-1]}
-
1
@velocity_stddev = stddev(@velocity)
-
end
-
-
1
@product_backlog = RbStory.product_backlog(@project, 10)
-
-
1
hours_per_point = []
-
1
@all_sprints.each {|sprint|
-
4
sprint.stories.each {|story|
-
2
bd = story.burndown
-
2
h = bd[:hours][0]
-
2
p = bd[:points][0]
-
2
next unless h && p && p != 0
-
hours_per_point << (h / p.to_f)
-
}
-
}
-
1
@hours_per_point_stddev = stddev(hours_per_point)
-
1
@hours_per_point = hours_per_point.sum.to_f / hours_per_point.size unless hours_per_point.size == 0
-
-
1
Statistics.active_tests.sort.each{|m|
-
9
r = send(m.intern)
-
9
next if r.nil? # this test deems itself irrelevant
-
@statistics[r ? :succeeded : :failed] <<
-
8
(m.to_s.gsub(/^test_/, '') + (r ? '' : '_failed'))
-
}
-
1
Statistics.stats.sort.each{|m|
-
5
v = send(m.intern)
-
@statistics[:values][m.to_s.gsub(/^stat_/, '')] =
-
v unless
-
v.nil? ||
-
5
(v.respond_to?(:"nan?") && v.nan?) ||
-
(v.respond_to?(:"infinite?") && v.infinite?)
-
}
-
-
1
if @statistics[:succeeded].size == 0 && @statistics[:failed].size == 0
-
@score = 100 # ?
-
else
-
1
@score = (@statistics[:succeeded].size * 100) / (@statistics[:succeeded].size + @statistics[:failed].size)
-
end
-
end
-
-
1
attr_reader :statistics, :score
-
1
attr_reader :active_sprint, :past_sprints
-
1
attr_reader :hours_per_point
-
-
1
def stddev(values)
-
2
median = values.sum / values.size.to_f
-
6
variance = 1.0 / (values.size * values.inject(0){|acc, v| acc + (v-median)**2})
-
2
return Math.sqrt(variance)
-
end
-
-
1
def self.available
-
return Statistics.instance_methods.select{|m| m =~ /^test_/}.collect{|m| m.split('_', 2).collect{|s| s.intern}}
-
end
-
-
1
def self.active_tests
-
# test this!
-
196
return Statistics.instance_methods.select{|m| m =~ /^test_/}.reject{|m| Backlogs.setting["disable_stats_#{m}".intern] }
-
end
-
-
1
def self.active
-
return Statistics.active_tests.collect{|m| m.split('_', 2).collect{|s| s.intern}}
-
end
-
-
1
def self.stats
-
187
return Statistics.instance_methods.select{|m| m =~ /^stat_/}
-
end
-
-
1
def info_no_active_sprint
-
return !@active_sprint
-
end
-
-
1
def test_product_backlog_filled
-
1
return (@project.status != Project::STATUS_ACTIVE || @product_backlog.length != 0)
-
end
-
-
1
def test_product_backlog_sized
-
2
return !@product_backlog.detect{|s| s.story_points.blank? }
-
end
-
-
1
def test_sprints_sized
-
5
return !Issue.exists?(["story_points is null and fixed_version_id in (?) and tracker_id in (?)", @all_sprints.collect{|s| s.id}, RbStory.trackers])
-
end
-
-
1
def test_sprints_estimated
-
5
return !Issue.exists?(["estimated_hours is null and fixed_version_id in (?) and tracker_id = ?", @all_sprints.collect{|s| s.id}, RbTask.tracker])
-
end
-
-
1
def test_sprint_notes_available
-
2
return !@past_sprints.detect{|s| !s.has_wiki_page}
-
end
-
-
1
def test_active
-
1
return (@project.status != Project::STATUS_ACTIVE || (@active_sprint && @active_sprint.activity))
-
end
-
-
1
def test_yield
-
1
accepted = []
-
1
@past_sprints.each {|sprint|
-
4
bd = sprint.burndown('up')
-
4
c = bd[:points_committed][-1]
-
4
a = bd[:points_accepted][-1]
-
4
next unless c && a && c != 0
-
-
accepted << [(a * 100.0) / c, 100.0].min
-
}
-
1
return false if accepted == []
-
return (stddev(accepted) < 10) # magic number
-
end
-
-
1
def test_committed_velocity_stable
-
1
return (@velocity_stddev && @velocity_stddev < 4) # magic number!
-
end
-
-
1
def test_sizing_consistent
-
1
return (@hours_per_point_stddev < 4) # magic number
-
end
-
-
1
def stat_sprints
-
1
return @past_sprints.size
-
end
-
-
1
def stat_velocity
-
1
return nil unless @velocity && @velocity.size > 0
-
1
return @velocity.sum / @velocity.size
-
end
-
-
1
def stat_velocity_stddev
-
1
return @velocity_stddev
-
end
-
-
1
def stat_sizing_stddev
-
1
return @hours_per_point_stddev
-
end
-
-
1
def stat_hours_per_point
-
1
return @hours_per_point
-
end
-
end
-
-
1
module ProjectPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
include Backlogs::ActiveRecord::Attributes
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
-
1
def scrum_statistics(force = false)
-
1
if force
-
# done this way to the potentially very expensive cache rebuild is done while the old cache may still be served to others
-
stats = Backlogs::Statistics.new(self)
-
Rails.cache.delete("Project(#{self.id}).scrum_statistics")
-
return Rails.cache.fetch("Project(#{self.id}).scrum_statistics", {:expires_in => 4.hours}) { stats }
-
end
-
## pretty expensive to compute, so if we're calling this multiple times, return the cached results
-
2
@scrum_statistics ||= Rails.cache.fetch("Project(#{self.id}).scrum_statistics", {:expires_in => 4.hours}) { Backlogs::Statistics.new(self) }
-
-
1
return @scrum_statistics
-
end
-
-
1
def projects_in_shared_product_backlog
-
#sharing off: only the product itself is in the product backlog
-
#sharing on: subtree is included in the product backlog
-
16
if Backlogs.setting[:sharing_enabled]
-
15
self.self_and_descendants.active
-
else
-
1
[self]
-
end
-
#TODO have an explicit association map which project shares its issues into other product backlogs
-
end
-
-
#return sprints which are
-
# 1. open in project,
-
# 2. share to project,
-
# 3. share to project but are scoped to project and subprojects
-
#depending on sharing mode
-
1
def open_shared_sprints
-
143
if Backlogs.setting[:sharing_enabled]
-
347
shared_versions.scoped(:conditions => {:status => ['open', 'locked']}, :order => 'sprint_start_date ASC, effective_date ASC').collect{|v| v.becomes(RbSprint) }
-
else #no backlog sharing
-
38
RbSprint.open_sprints(self)
-
end
-
end
-
-
#depending on sharing mode
-
1
def closed_shared_sprints
-
63
if Backlogs.setting[:disable_closed_sprints_to_master_backlogs]
-
return []
-
else
-
63
if Backlogs.setting[:sharing_enabled]
-
42
shared_versions.scoped(:conditions => {:status => ['closed']}, :order => 'sprint_start_date ASC, effective_date ASC').collect{|v| v.becomes(RbSprint) }
-
else #no backlog sharing
-
32
RbSprint.closed_sprints(self)
-
end
-
end #disable_closed
-
end
-
-
end
-
end
-
end
-
-
1
Project.send(:include, Backlogs::ProjectPatch) unless Project.included_modules.include? Backlogs::ProjectPatch
-
1
require_dependency 'query'
-
1
require 'erb'
-
-
1
module Backlogs
-
1
class RbERB
-
1
def initialize(s)
-
@sql = ERB.new(s)
-
end
-
-
1
def to_s
-
return @sql.result
-
end
-
end
-
-
1
module QueryPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
# Same as typing in the class
-
1
base.class_eval do
-
1
unloadable # Send unloadable so it will not be unloaded in development
-
1
base.add_available_column(QueryColumn.new(:story_points, :sortable => "#{Issue.table_name}.story_points"))
-
1
base.add_available_column(QueryColumn.new(:velocity_based_estimate))
-
1
base.add_available_column(QueryColumn.new(:position, :sortable => "#{Issue.table_name}.position"))
-
1
base.add_available_column(QueryColumn.new(:remaining_hours, :sortable => "#{Issue.table_name}.remaining_hours"))
-
-
1
alias_method_chain :available_filters, :backlogs_issue_type
-
1
alias_method_chain :sql_for_field, :backlogs_issue_type
-
end
-
end
-
-
1
module InstanceMethods
-
1
def available_filters_with_backlogs_issue_type
-
367
@available_filters = available_filters_without_backlogs_issue_type
-
-
367
if RbStory.trackers.length == 0 or RbTask.tracker.blank?
-
backlogs_filters = { }
-
else
-
367
backlogs_filters = {
-
"backlogs_issue_type" => { :type => :list,
-
:values => [[l(:backlogs_story), "story"], [l(:backlogs_task), "task"], [l(:backlogs_impediment), "impediment"], [l(:backlogs_any), "any"]],
-
:order => 20 }
-
}
-
end
-
-
367
return @available_filters.merge(backlogs_filters)
-
end
-
-
1
def sql_for_field_with_backlogs_issue_type(field, operator, value, db_table, db_field, is_custom_filter=false)
-
64
return sql_for_field_without_backlogs_issue_type(field, operator, value, db_table, db_field, is_custom_filter) unless field == "backlogs_issue_type"
-
-
18
db_table = Issue.table_name
-
-
18
sql = []
-
-
18
selected_values = values_for(field)
-
18
selected_values = ['story', 'task'] if selected_values.include?('any')
-
-
18
story_trackers = RbStory.trackers(:type=>:string)
-
54
all_trackers = (RbStory.trackers + [RbTask.tracker]).collect{|val| "#{val}"}.join(",")
-
-
18
selected_values.each { |val|
-
28
case val
-
when "story"
-
16
sql << "(#{db_table}.tracker_id in (#{story_trackers}))"
-
-
when "task"
-
10
sql << "(#{db_table}.tracker_id = #{RbTask.tracker})"
-
-
when "impediment"
-
sql << "(#{db_table}.id in (
-
select issue_from_id
-
from issue_relations ir
-
join issues blocked on
-
blocked.id = ir.issue_to_id
-
and blocked.tracker_id in (#{all_trackers})
-
where ir.relation_type = 'blocks'
-
2
))"
-
end
-
}
-
-
18
case operator
-
when "="
-
18
sql = sql.join(" or ")
-
-
when "!"
-
sql = "not (" + sql.join(" or ") + ")"
-
end
-
-
18
return sql
-
end
-
end
-
-
1
module ClassMethods
-
# Setter for +available_columns+ that isn't provided by the core.
-
1
def available_columns=(v)
-
self.available_columns = (v)
-
end
-
-
# Method to add a column to the +available_columns+ that isn't provided by the core.
-
1
def add_available_column(column)
-
self.available_columns << (column)
-
end
-
end
-
end
-
end
-
-
1
Query.send(:include, Backlogs::QueryPatch) unless Query.included_modules.include? Backlogs::QueryPatch
-
1
require 'rubygems'
-
1
require 'yaml'
-
1
require 'singleton'
-
-
1
unless defined?('ReliableTimout') || defined?(:ReliableTimout)
-
if Backlogs.gems.include?('system_timer')
-
require 'system_timer'
-
ReliableTimout = SystemTimer
-
else
-
require 'timeout'
-
ReliableTimout = Timeout
-
end
-
end
-
-
1
module Backlogs
-
1
def version
-
root = File.expand_path('..', File.dirname(__FILE__))
-
git = File.join(root, '.git')
-
v = Redmine::Plugin.find(:redmine_backlogs).version
-
-
g = nil
-
if File.directory?(git)
-
Dir.chdir(root)
-
g = `git log | head -1 | awk '{print $2}'`
-
g.strip!
-
g = "(#{g})"
-
end
-
-
v = [v, g].compact.join(' ')
-
v = '?' if v == ''
-
return v
-
end
-
1
module_function :version
-
-
1
def development?
-
return !Rails.env.production?
-
end
-
1
module_function :"development?"
-
-
1
def platform_support(raise_error = false)
-
3460
supported = Rails.cache.fetch("Backlogs.platform_supported", {:expires_in => 24.hours}) {
-
154
versions = nil # needed so versions isn't block-scoped in the timeout
-
154
begin
-
154
ReliableTimout.timeout(10) { versions = YAML::load(open('http://www.redminebacklogs.net/versions.yml').read) }
-
rescue
-
154
versions = YAML::load(File.open(File.join(File.dirname(__FILE__), 'versions.yml')).read)
-
end
-
154
versions
-
}
-
-
3460
return "You are running backlogs #{Redmine::Plugin.find(:redmine_backlogs).version}, latest version is #{supported[:backlogs]}" if Redmine::Plugin.find(:redmine_backlogs).version != supported[:backlogs]
-
-
3460
supported = supported[platform]
-
3460
raise "Unsupported platform #{platform}" unless supported
-
-
17300
currentversion = Redmine::VERSION.to_a.collect{|d| d.to_s}
-
3460
r = RUBY_VERSION.split('.')
-
3460
supported.each{|version|
-
3460
v = version[:version].split('.')
-
3460
next unless currentversion[0,v.length] == v
-
-
3460
v = version[:ruby].split('.')
-
3460
next unless r[0,v.length] == v
-
-
3460
return "#{Redmine::VERSION}#{version[:unsupported] ? '(unsupported but might work)' : ''}"
-
}
-
-
return "#{Redmine::VERSION} (DEVELOPMENT MODE)" if development?
-
-
msg = "#{Redmine::VERSION} on #{RUBY_VERSION} (NOT SUPPORTED; please install #{platform} #{supported.reject{|v| v[:unsupported]}.collect{|v| "#{v[:version]} on #{v[:ruby]}"}.uniq.sort.join(' / ')}"
-
raise msg if raise_error
-
return msg
-
end
-
1
module_function :platform_support
-
-
1
def os
-
3460
return :windows if RUBY_PLATFORM =~ /cygwin|windows|mswin|mingw|bccwin|wince|emx/
-
3460
return :unix if RUBY_PLATFORM =~ /darwin|linux/
-
return :java if RUBY_PLATFORM =~ /java/
-
return nil
-
end
-
1
module_function :os
-
-
1
def gems
-
27680
installed = Hash[*(['json', 'system_timer', 'nokogiri', 'open-uri/cached', 'holidays', 'icalendar', 'prawn'].collect{|gem| [gem, false]}.flatten)]
-
3460
installed.delete('system_timer') unless os == :unix && RUBY_VERSION =~ /^1\.8\./
-
3460
installed.keys.each{|gem|
-
20760
begin
-
20760
require gem
-
20760
installed[gem] = true
-
rescue LoadError
-
end
-
}
-
3460
return installed
-
end
-
1
module_function :gems
-
-
1
def trackers
-
3460
return {:task => !RbTask.tracker.nil?, :story => RbStory.trackers.size != 0, :default_priority => !IssuePriority.default.nil?}
-
end
-
1
module_function :trackers
-
-
1
def task_workflow(project)
-
28
return false unless RbTask.tracker
-
-
28
roles = User.current.roles_for_project(@project)
-
28
tracker = Tracker.find(RbTask.tracker)
-
-
28
[false, true].each{|creator|
-
56
[false, true].each{|assignee|
-
112
tracker.issue_statuses.each {|status|
-
672
status.new_statuses_allowed_to(roles, tracker, creator, assignee).each{|s|
-
return true
-
}
-
}
-
}
-
}
-
end
-
1
module_function :task_workflow
-
-
1
def migrated?
-
62280
available = Dir[File.join(File.dirname(__FILE__), '../db/migrate/*.rb')].collect{|m| Integer(File.basename(m).split('_')[0].gsub(/^0+/, ''))}.sort
-
3460
return true if available.size == 0
-
3460
available = available[-1]
-
-
3460
ran = []
-
3460
Setting.connection.execute("select version from schema_migrations where version like '%-redmine_backlogs'").each{|m|
-
58820
ran << Integer((m.is_a?(Hash) ? m.values : m)[0].split('-')[0])
-
}
-
3460
return false if ran.size == 0
-
3460
ran = ran.sort[-1]
-
-
3460
return ran >= available
-
end
-
1
module_function :migrated?
-
-
1
def configured?(project=nil)
-
24220
return false if Backlogs.gems.values.reject{|installed| installed}.size > 0
-
13840
return false if Backlogs.trackers.values.reject{|configured| configured}.size > 0
-
3460
return false unless Backlogs.migrated?
-
3460
return false unless project.nil? || project.enabled_module_names.include?("backlogs")
-
3460
begin
-
3460
platform_support(true)
-
rescue
-
return false
-
end
-
-
3460
return true
-
end
-
1
module_function :configured?
-
-
1
def platform
-
4964
unless @platform
-
1
begin
-
1
ChiliProject::VERSION
-
@platform = :chiliproject
-
rescue NameError
-
1
@platform = :redmine
-
end
-
end
-
4964
return @platform
-
end
-
1
module_function :platform
-
-
1
class SettingsProxy
-
1
include Singleton
-
-
1
def [](key)
-
21377
return safe_load[key]
-
end
-
-
1
def []=(key, value)
-
549
settings = safe_load
-
549
settings[key] = value
-
549
Setting.plugin_redmine_backlogs = settings
-
end
-
-
1
def to_h
-
1016
h = safe_load
-
1016
h.freeze
-
1016
h
-
end
-
-
1
private
-
-
1
def safe_load
-
# At the first migration, the settings table will not exist
-
22942
return {} unless Setting.table_exists?
-
-
22942
settings = Setting.plugin_redmine_backlogs.dup
-
22942
if settings.is_a?(String)
-
Rails.logger.error "Unable to load settings"
-
return {}
-
end
-
22942
settings
-
end
-
end
-
-
1
def setting
-
21926
SettingsProxy.instance
-
end
-
1
module_function :setting
-
1
def settings
-
1016
SettingsProxy.instance.to_h
-
end
-
1
module_function :settings
-
end
-
1
require_dependency 'user'
-
-
1
module Backlogs
-
1
module TrackerPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def backlog?
-
return (issue_statuses.collect{|s| s.backlog}.compact.uniq.size == 4)
-
end
-
-
1
def status_for_done_ratio(r)
-
return (issue_statuses.select{|s| !s.default_done_ratio.nil? && s.default_done_ratio < r && !s.is_closed?}.sort{|a, b| b.default_done_ratio <=> a.default_done_ratio} + [nil])[0]
-
end
-
end
-
end
-
end
-
-
1
Tracker.send(:include, Backlogs::TrackerPatch) unless Tracker.included_modules.include? Backlogs::TrackerPatch
-
1
require_dependency 'user'
-
-
1
module Backlogs
-
1
class Preference
-
1
def initialize(user)
-
88
@user = user
-
88
@prefs = {}
-
end
-
-
1
def []=(attr, value)
-
88
prefixed = "backlogs_#{attr}".intern
-
-
88
case attr
-
when :task_color
-
88
value = value.to_s.strip
-
88
value = "##{value}" if value =~ /^[0-9A-F]{6}$/i
-
88
raise "Color format must be 6 hex digit string or empty, supplied value: #{value.inspect}" unless value == '' || value =~ /^#[0-9A-F]{6}$/i
-
88
value.upcase!
-
else
-
raise "Unsupported attribute '#{attr}'"
-
end
-
-
88
@user.pref[prefixed] = value
-
88
@prefs[prefixed] = value
-
88
@user.pref.save!
-
end
-
-
1
def [](attr)
-
264
prefixed = "backlogs_#{attr}".intern
-
-
264
unless @prefs.include?(prefixed)
-
176
value = @user.pref[prefixed].to_s.strip
-
-
176
case attr
-
when :task_color
-
88
if value == '' # assign default
-
792
colors = UserPreference.find(:all).collect{|p| p[prefixed].to_s.upcase}.select{|p| p != ''}
-
88
min = 0x999999
-
88
50.times do
-
88
candidate = "##{(min + rand(0xFFFFFF-min)).to_s(16).upcase}"
-
88
next if colors.include?(candidate)
-
88
value = candidate
-
88
break
-
end
-
88
self[attr] = value
-
end
-
-
when :task_color_light
-
88
value = self[:task_color].to_s
-
88
value = Backlogs::Color.new(value).lighten(0.5) unless value == ''
-
-
else
-
raise "Unsupported attribute '#{attr}'"
-
end
-
-
176
@prefs[prefixed] = value
-
end
-
-
264
return @prefs[prefixed]
-
end
-
end
-
-
1
module UserPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
-
1
def backlogs_preference
-
176
@backlogs_preference ||= Backlogs::Preference.new(self)
-
end
-
-
end
-
end
-
end
-
-
1
User.send(:include, Backlogs::UserPatch) unless User.included_modules.include? Backlogs::UserPatch
-
1
require_dependency 'version'
-
-
1
module Backlogs
-
1
module VersionPatch
-
1
def self.included(base) # :nodoc:
-
1
base.extend(ClassMethods)
-
1
base.send(:include, InstanceMethods)
-
-
1
base.class_eval do
-
1
unloadable
-
-
1
include Backlogs::ActiveRecord::Attributes
-
end
-
end
-
-
1
module ClassMethods
-
end
-
-
1
module InstanceMethods
-
1
def burndown
-
return RbSprint.find_by_id(self.id).burndown
-
end
-
-
end
-
end
-
end
-
-
1
Version.send(:include, Backlogs::VersionPatch) unless Version.included_modules.include? Backlogs::VersionPatch
-
# Copyright (c) 2007 McClain Looney
-
#
-
# Permission is hereby granted, free of charge, to any person obtaining a copy
-
# of this software and associated documentation files (the "Software"), to deal
-
# in the Software without restriction, including without limitation the rights
-
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-
# copies of the Software, and to permit persons to whom the Software is
-
# furnished to do so, subject to the following conditions:
-
#
-
# The above copyright notice and this permission notice shall be included in
-
# all copies or substantial portions of the Software.
-
#
-
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-
# THE SOFTWARE.
-
-
-
# Implements a color (r,g,b + a) with conversion to/from web format (eg #aabbcc), and
-
# with a number of utilities to lighten, darken and blend values.
-
1
module Backlogs
-
1
class Color
-
-
1
attr_reader :r, :g, :b, :a
-
-
# Table for conversion to hex
-
1
HEXVAL = (('0'..'9').to_a).concat(('A'..'F').to_a).freeze
-
# Default value for #darken, #lighten etc.
-
1
BRIGHTNESS_DEFAULT = 0.2
-
-
# Constructor. Inits to white (#FFFFFF) by default, or accepts any params
-
# supported by #parse.
-
1
def initialize(*args)
-
266
@r = 255
-
266
@g = 255
-
266
@b = 255
-
266
@a = 255
-
-
266
if args.size.between?(3,4)
-
90
self.r = args[0]
-
90
self.g = args[1]
-
90
self.b = args[2]
-
90
self.a = args[3] if args[3]
-
else
-
176
set(*args)
-
end
-
end
-
-
# All-purpose setter - pass in another Color, '#000000', rgb vals... whatever
-
1
def set(*args)
-
176
val = Color.parse(*args)
-
176
unless val.nil?
-
176
self.r = val.r
-
176
self.g = val.g
-
176
self.b = val.b
-
176
self.a = val.a
-
end
-
176
self
-
end
-
-
# Test for equality, accepts string vals as well, eg Color.new('aaa') == '#AAAAAA' => true
-
1
def ==(val)
-
val = Color.parse(val)
-
return false if val.nil?
-
return r == val.r && g == val.g && b == val.b && a == val.a
-
end
-
-
# Setters for individual channels - take 0-255 or '00'-'FF' values
-
355
def r=(val); @r = from_hex(val); end
-
355
def g=(val); @g = from_hex(val); end
-
355
def b=(val); @b = from_hex(val); end
-
265
def a=(val); @a = from_hex(val); end
-
-
# Attempt to read in a string and parse it into values
-
1
def self.parse(*args)
-
176
case args.size
-
-
when 0 then
-
return nil
-
-
when 1 then
-
176
val = args[0]
-
-
# Trivial parse... :-)
-
176
return val if val.is_a?(Color)
-
-
# Single value, assume grayscale
-
88
return Color.new(val, val, val) if val.is_a?(Fixnum)
-
-
# Assume string
-
88
str = val.to_s.upcase
-
88
str = str[/[0-9A-F]{3,8}/] || ''
-
88
case str.size
-
when 3, 4 then
-
r, g, b, a = str.scan(/[0-9A-F]/)
-
when 6,8 then
-
88
r, g, b, a = str.scan(/[0-9A-F]{2}/)
-
else
-
return nil
-
end
-
-
88
return Color.new(r,g,b,a || 255)
-
-
when 3,4 then
-
return Color.new(*args)
-
-
end
-
nil
-
end
-
-
1
def inspect
-
to_s(true)
-
end
-
-
1
def to_s(add_hash = true)
-
88
trans? ? to_rgba(add_hash) : to_rgb(add_hash)
-
end
-
-
1
def to_rgb(add_hash = true)
-
88
(add_hash ? '#' : '') + to_hex(r) + to_hex(g) + to_hex(b)
-
end
-
-
1
def to_rgba(add_hash = true)
-
to_rgb(add_hash) + to_hex(a)
-
end
-
-
1
def opaque?
-
@a == 255
-
end
-
-
1
def trans?
-
88
@a != 255
-
end
-
-
1
def grayscale?
-
@r == @g && @g == @b
-
end
-
-
# Lighten color towards white. 0.0 is a no-op, 1.0 will return #FFFFFF
-
1
def lighten(amt = BRIGHTNESS_DEFAULT)
-
88
return self if amt <= 0
-
88
return WHITE if amt >= 1.0
-
88
val = Color.new(self)
-
88
val.r += ((255-val.r) * amt).to_i
-
88
val.g += ((255-val.g) * amt).to_i
-
88
val.b += ((255-val.b) * amt).to_i
-
88
val
-
end
-
-
# In place version of #lighten
-
1
def lighten!(amt = BRIGHTNESS_DEFAULT)
-
set(lighten(amt))
-
self
-
end
-
-
# Darken a color towards full black. 0.0 is a no-op, 1.0 will return #000000
-
1
def darken(amt = BRIGHTNESS_DEFAULT)
-
return self if amt <= 0
-
return BLACK if amt >= 1.0
-
val = Color.new(self)
-
val.r -= (val.r * amt).to_i
-
val.g -= (val.g * amt).to_i
-
val.b -= (val.b * amt).to_i
-
val
-
end
-
-
# In place version of #darken
-
1
def darken!(amt = BRIGHTNESS_DEFAULT)
-
set(darken(amt))
-
self
-
end
-
-
# Convert to grayscale, using perception-based weighting
-
1
def grayscale
-
val = Color.new(self)
-
val.r = val.g = val.b = (0.2126 * val.r + 0.7152 * val.g + 0.0722 * val.b)
-
val
-
end
-
-
# In place version of #grayscale
-
1
def grayscale!
-
set(grayscale)
-
self
-
end
-
-
# Blend to a color amt % towards another color value, eg
-
# red.blend(blue, 0.5) will be purple, white.blend(black, 0.5) will be gray, etc.
-
1
def blend(other, amt)
-
other = Color.parse(other)
-
return Color.new(self) if amt <= 0 || other.nil?
-
return Color.new(other) if amt >= 1.0
-
val = Color.new(self)
-
val.r += ((other.r - val.r)*amt).to_i
-
val.g += ((other.g - val.g)*amt).to_i
-
val.b += ((other.b - val.b)*amt).to_i
-
val
-
end
-
-
# In place version of #blend
-
1
def blend!(other, amt)
-
set(blend(other, amt))
-
self
-
end
-
-
# Class-level version for explicit blends of two values, useful with constants
-
1
def self.blend(col1, col2, amt)
-
col1 = Color.parse(col1)
-
col2 = Color.parse(col2)
-
col1.blend(col2, amt)
-
end
-
-
1
protected
-
-
# Convert int to string hex, eg 255 => 'FF'
-
1
def to_hex(val)
-
264
HEXVAL[val / 16] + HEXVAL[val % 16]
-
end
-
-
# Convert int or string to int, eg 80 => 80, 'FF' => 255, '7' => 119
-
1
def from_hex(val)
-
1326
if val.is_a?(String)
-
# Double up if single char form
-
264
val = val + val if val.size == 1
-
# Convert to integer
-
264
val = val.hex
-
end
-
# Clamp
-
1326
val = 0 if val < 0
-
1326
val = 255 if val > 255
-
1326
val
-
end
-
-
1
public
-
-
# Some constants for general use
-
1
WHITE = Color.new(255,255,255).freeze
-
1
BLACK = Color.new(0,0,0).freeze
-
-
end
-
-
# "Global" method for creating Color objects, eg:
-
# new_color = rgb(params[:new_color])
-
# style="border: 1px solid <%= rgb(10,50,80).lighten %>"
-
1
def rgb(*args)
-
Color.parse(*args)
-
end
-
1
module_function :rgb
-
end
-
1
require 'redmine'
-
-
1
Rails.configuration.to_prepare do
-
1
require 'grit'
-
1
require 'gollum'
-
1
require_dependency 'redmine_gollum_patches/gollum_project_model_patch'
-
1
require_dependency 'redmine_gollum_patches/gollum_projects_helper_patch'
-
1
require_dependency 'redmine_gollum_patches/gollum_projects_controller_patch'
-
-
# project model should be patched before projects controller
-
1
patches = [ [Project, RedmineGollum::Patches::Project],
-
[ProjectsController, RedmineGollum::Patches::ProjectsController],
-
[ProjectsHelper, RedmineGollum::Patches::ProjectsHelper] ]
-
1
patches.each{|patch|
-
3
unless patch[0].included_modules.include?(patch[1])
-
3
patch[0].send(:include, patch[1])
-
end
-
}
-
end
-
-
1
Redmine::Plugin.register :redmine_gollum do
-
1
name 'Redmine Gollum plugin'
-
1
author 'Kang-min Liu <gugod@gugod.org>'
-
1
description 'A gollum plugin for redmine'
-
-
# use git to get version name
-
1
repo = Grit::Repo.new("#{Rails.root}/plugins/redmine_gollum/.git")
-
1
version repo.recent_tag_name('HEAD', :tags=>true)
-
-
1
url 'https://github.com/gugod/redmine-gollum/'
-
1
author_url 'http://gugod.org'
-
-
1
requires_redmine :version_or_higher => '2.0.2'
-
-
settings :default => {
-
:gollum_base_path => Pathname.new(Rails.root + "gollum")
-
},
-
1
:partial => 'shared/settings'
-
-
1
project_module :gollum_pages do
-
1
permission :view_gollum_pages, :gollum_pages => [:index, :show]
-
1
permission :add_gollum_pages, :gollum_pages => [:new, :create]
-
1
permission :edit_gollum_pages, :gollum_pages => [:edit, :update]
-
1
permission :delete_gollum_pages, :gollum_pages => [:destroy]
-
-
1
permission :manage_gollum_wiki, :gollum_wikis => [:index,:show, :create, :update]
-
end
-
-
1
menu :project_menu, :gollum_pages, { :controller => :gollum_pages, :action => :index }, :caption => 'Gollum', :before => :wiki, :param => :project_id
-
end
-
1
require_dependency 'project'
-
-
1
module RedmineGollum
-
1
module Patches
-
1
module Project
-
-
1
def self.included(base) # :nodoc:
-
1
base.send(:include, InstanceMethods)
-
end
-
-
1
module InstanceMethods
-
1
def gollum_wiki
-
raise "Need project id." unless self.id
-
return GollumWiki.first(:conditions => ["project_id = ?", self.id]) ||
-
GollumWiki.new( :project_id => self.id,
-
:git_path => Setting.plugin_redmine_gollum[:gollum_base_path].to_s + "/#{self.identifier}.wiki.git".to_s)
-
end
-
end
-
-
end
-
end
-
end
-
1
require_dependency 'projects_controller'
-
-
1
module RedmineGollum
-
1
module Patches
-
1
module ProjectsController
-
-
1
def self.included(base) # :nodoc:
-
1
base.send(:include, InstanceMethods)
-
1
base.class_eval do
-
1
alias_method_chain :settings, :gollum_settings
-
end
-
end
-
-
1
module InstanceMethods
-
1
def settings_with_gollum_settings
-
settings_without_gollum_settings
-
@gollum_wiki = @project.gollum_wiki
-
end
-
end
-
-
end
-
end
-
end
-
1
require_dependency 'projects_helper'
-
-
1
module RedmineGollum
-
1
module Patches
-
1
module ProjectsHelper
-
-
1
def self.included(base) # :nodoc:
-
1
base.send(:include, InstanceMethods)
-
1
base.class_eval do
-
1
alias_method_chain :project_settings_tabs, :gollum_tab
-
end
-
end
-
-
1
module InstanceMethods
-
# Adds a gollum tab to the project settings page
-
1
def project_settings_tabs_with_gollum_tab
-
tabs = project_settings_tabs_without_gollum_tab
-
tabs << { :name => 'gollum', :action => :manage_gollum_wiki, :partial => 'gollum_wikis/edit', :label => 'Gollum' }
-
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
-
end
-
end
-
-
end
-
end
-
end